• Stars
    star
    222
  • Rank 172,943 (Top 4 %)
  • Language
    JavaScript
  • License
    MIT License
  • Created almost 7 years ago
  • Updated over 1 year ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Dead simple React state management to bring pure components alive

React Organism

Travis npm package Coveralls

Dead simple React/Preact state management to bring pure components alive

  • Supports async/await and easy loading (e.g. fetch())
  • Reload when particular props change
  • Animate using generator functions: just yield the new state for each frame
  • Tiny: 1.69 KB gzipped (3.49 KB uncompressed)
  • Embraces the existing functional setState while avoiding boilerplate (no writing this.setState() or .bind again)
  • Easy to unit test

Table of contents

Installation

npm i react-organism --save

Demos

Usage

Basic

// organisms/Counter.js
import makeOrganism from 'react-organism'
import Counter from './components/Counter'

export default makeOrganism(Counter, {
  initial: () => ({ count: 0 }),
  increment: () => ({ count }) => ({ count: count + 1 }),
  decrement: () => ({ count }) => ({ count: count - 1 })
})
// components/Counter.js
import React, { Component } from 'react'

export default function Counter({
  count,
  handlers: {
    increment,
    decrement
  }
}) {
  return (
    <div>
      <button onClick={ decrement } children='−' />
      <span>{ count }</span>
      <button onClick={ increment } children='+' />
    </div>
  )
}

Using props

The handlers can easily use props, which are always passed as the first argument

// organisms/Counter.js
import makeOrganism from 'react-organism'
import Counter from './components/Counter'

export default makeOrganism(Counter, {
  initial: ({ initialCount = 0 }) => ({ count: initialCount }),
  increment: ({ stride = 1 }) => ({ count }) => ({ count: count + stride }),
  decrement: ({ stride = 1 }) => ({ count }) => ({ count: count - stride })
})

// Render passing prop: <CounterOrganism stride={ 20 } />

Async

Asynchronous code to load from an API is easy:

// components/Items.js
import React, { Component } from 'react'

export default function Items({
  items,
  collectionName,
  handlers: {
    load
  }
}) {
  return (
    <div>
      {
        !!items ? (
          `${items.length} ${collectionName}`
        ) : (
          'Loading…'
        )
      }
      <div>
        <button onClick={ load } children='Reload' />
      </div>
    </div>
  )
}
// organisms/Items.js
import makeOrganism from 'react-organism'
import Items from '../components/Items'

const baseURL = 'https://jsonplaceholder.typicode.com'
const fetchAPI = (path) => fetch(baseURL + path).then(r => r.json())

export default makeOrganism(Items, {
  initial: () => ({ items: null }),

  load: async ({ path }, prevProps) => {
    if (!prevProps || path !== prevProps.path) {
      return { items: await fetchAPI(path) }
    }
  }
})
<div>
  <ItemsOrganism path='/photos' collectionName='photos' />
  <ItemsOrganism path='/todos' collectionName='todo items' />
</div>

Handling events

Handlers can easily accept arguments such as events.

// components/Calculator.js
import React, { Component } from 'react'

export default function Calculator({
  value,
  handlers: {
    changeValue,
    double,
    add3,
    initial
  }
}) {
  return (
    <div>
      <input value={ value } onChange={ changeValue } />
      <button onClick={ double } children='Double' />
      <button onClick={ add3 } children='Add 3' />
      <button onClick={ initial } children='reset' />
    </div>
  )
}
// organisms/Calculator.js
import makeOrganism from 'react-organism'
import Calculator from '../components/Calculator'

export default makeOrganism(Calculator, {
  initial: ({ initialValue = 0 }) => ({ value: initialValue }),
  // Destructure event to get target
  changeValue: (props, { target }) => ({ value }) => ({ value: parseInt(target.value, 10) }),
  double: () => ({ value }) => ({ value: value * 2 }),
  add3: () => ({ value }) => ({ value: value + 3 })
})

Animation

import makeOrganism from 'react-organism'
import Counter from '../components/Counter'

export default makeOrganism(Counter, {
  initial: ({ initialCount = 0 }) => ({ count: initialCount }),
  increment: function * ({ stride = 20 }) {
    while (stride > 0) {
      yield ({ count }) => ({ count: count + 1 })
      stride -= 1
    }
  },
  decrement: function * ({ stride = 20 }) {
    while (stride > 0) {
      yield ({ count }) => ({ count: count - 1 })
      stride -= 1
    }
  }
})

Automatically extract from data- attributes and <forms>

Example coming soon

Serialization: Local storage

// organisms/Counter.js
import makeOrganism from 'react-organism'
import Counter from '../components/Counter'

const localStorageKey = 'counter'

export default makeOrganism(Counter, {
  initial: ({ initialCount = 0 }) => ({ count: initialCount }),
  load: async (props, prevProps) => {
    if (!prevProps) {
      // Try commenting out:
      /* throw (new Error('Oops!')) */

      // Load previously stored state, if present
      return await JSON.parse(localStorage.getItem(localStorageKey))
    }
  },
  increment: ({ stride = 1 }) => ({ count }) => ({ count: count + stride }),
  decrement: ({ stride = 1 }) => ({ count }) => ({ count: count - stride })
}, {
  onChange(state) {
    // When state changes, save in local storage
    localStorage.setItem(localStorageKey, JSON.stringify(state))
  }
})

Separate and reuse state handlers

React Organism supports separating state handlers and the component into their own files. This means state handlers could be reused by multiple smart components.

Here’s an example of separating state:

// state/counter.js
export const initial = () => ({
  count: 0
})

export const increment = () => ({ count }) => ({ count: count + 1 })
export const decrement = () => ({ count }) => ({ count: count - 1 })
// organisms/Counter.js
import makeOrganism from 'react-organism'
import Counter from './components/Counter'
import * as counterState from './state/counter'

export default makeOrganism(Counter, counterState)
// App.js
import React from 'react'
import CounterOrganism from './organisms/Counter'

class App extends React.Component {
  render() {
    return (
      <div>
        <CounterOrganism />
      </div>
    )
  }
}

Multicelled Organisms

Example coming soon.

API

makeOrganism(PureComponent, StateFunctions, options?)

import makeOrganism from 'react-organism'

Creates a smart component, rendering using React component PureComponent, and managing state using StateFunctions.

PureComponent

A React component, usually a pure functional component. This component is passed as its props:

  • The props passed to the smart component, combined with
  • The current state, combined with
  • handlers which correspond to each function in StateFunctions and are ready to be passed to e.g. onClick, onChange, etc.
  • loadError?: Error produced by the load handler
  • handlerError?: Error produced by any other handler

StateFunctions

Object with functional handlers. See state functions below.

Either pass a object directly with each function, or create a separate file with each handler function exported out, and then bring in using import * as StateFunctions from '...'.

options

adjustArgs?(args: array) => newArgs: array

Used to enhance handlers. See built-in handlers below.

onChange?(state)

Called after the state has changed, making it ideal for saving the state somewhere (e.g. Local Storage).

State functions

Your state is handled by a collection of functions. Each function is pure: they can only rely on the props and state passed to them. Functions return the new state, either immediately or asynchronously.

Each handler is passed the current props first, followed by the called arguments:

  • (props, event): most event handlers, e.g. onClick, onChange
  • (props, first, second): e.g. handler(first, second)
  • (props, ...args): get all arguments passed
  • (props): ignore any arguments
  • (): ignore props and arguments

Handlers must return one of the following:

  • An object with new state changes, a la React’s setState(changes).
  • A function accepting the previous state and current props, and returns the new state, a la React’s setState((prevState, props) => changes).
  • A promise resolving to any of the above (object / function), which will then be used to update the state. Uncaught errors are stored in state under the key handlerError. Alternatively, your handler can use the async/await syntax.
  • An iterator, such as one made by using a generator function. Each object passed to yield may be one of the above (object / function / promise).
  • An array of any of the above (object / function / promise / iterator).
  • Or optionally, nothing.

There are some handlers for special tasks, specifically:

initial(props) => object (required)

Return initial state to start off with, a la React’s initialState. Passed props.

load(props: object, prevProps: object?, { handlers: object }) => object | Promise<object> | void (optional)

Passed the current props and the previous props. Return new state, a Promise returning new state, or nothing. You may also use a generator function (function * load(props, prevProps)) and yield state changes.

If this is the first time loaded or if being reloaded, then prevProps is null.

Usual pattern is to check for either prevProps being null or if the prop of interest has changed from its previous value:

export const load = async ({ id }, prevProps) => {
  if (!prevProps || id !== prevProps.id) {
    return { item: await loadItem(id) }
  }
}

Your load handler will be called in React’s lifecycle: componentDidMount and componentWillReceiveProps.

Argument enhancers

Handler arguments can be adjusted, to cover many common cases. Pass them to the adjustArgs option. The following enhancers are built-in:

extractFromDOM(args: array) => newArgs: array

import extractFromDOM from 'react-organism/lib/adjustArgs/extractFromDOM'

Extract values from DOM, specifically:

  • For events as the first argument, extracts value, checked, and name from event.target. Additionally, if target has data- attributes, these will also be extracted in camelCase from its dataset. Suffixing data- attributes with _number will convert value to a number (instead of string) using parseFloat, and drop the suffix. Handler will receive these extracted values in an object as the first argument, followed by the original arguments.
  • For submit events, extracts values of <input> fields in a <form>. Handler will receive the values keyed by the each input’s name attribute, followed by the original arguments. Pass the handler to the onSubmit prop of the <form>. Form must have data-extract attribute present. To clear the form after submit, add data-reset to the form.

Why instead of Redux?

  • Like Redux, separate your state management from rendering
  • Unlike Redux, avoid loose strings for identifying actions
  • Redux encourages having state in one bundle, whereas dynamic import() encourages breaking apps into sections
  • Easier to reuse functionality, as action handlers are totally encapsulated
  • No ability to reach across to the other side of your state tree
  • Encourages composition of components
  • Supports async and await in any action
  • Supports generator functions to allow multiple state changes — great for animation
  • No switch statements
  • No boilerplate or additional helper libraries needed

More Repositories

1

Lantern

Mac app for website auditing and crawling
Swift
209
star
2

soonerorlater

JavaScript parser of natural language time periods
TypeScript
90
star
3

Orb

Elixir DSL for WebAssembly
Elixir
53
star
4

flambeau

Opinionated Redux additions: declarative, pleasant action creators, reducer encapsulation, async support
JavaScript
44
star
5

BurntCocoaUI

Use Swift enums and structs with NSMenu, NSPopUpButton, NSSegmentedControl
Swift
29
star
6

dovetail

Fast server rendered components in Go. Inspired by SwiftUI, React, Elm.
Go
15
star
7

Symbolist

Look up symbols in Mac executables, and search Cocoa’s classes
Objective-C
13
star
8

glaze

Display HTML elements conveniently with escaping of attributes, text, URLs, email addresses.
PHP
8
star
9

OrbWasmtime

Run WebAssembly in Elixir via Wasmtime
Elixir
6
star
10

react-rx-subscriber

React component enhancer to easily subscribe to Rx Observables
JavaScript
6
star
11

bootstrap-next

Example of Bootstrap 4 with Next.js
JavaScript
6
star
12

ctss

Functional CSS meet TypeScript
TypeScript
6
star
13

Shohin

Pragmatic React/Elm-like components & state management for iOS
Swift
5
star
14

RoyalIcing

My personal site, built using Collected Press
HTML
5
star
15

astro-crud

Cookies, server rendering, and HTTP handler demo with Astro
Astro
5
star
16

asdf-versions

Install multiple versions of go, elixir, rust, node, etc easily with asdf
Makefile
4
star
17

react-ramda

Functional utilities for React component state
JavaScript
4
star
18

auditioner

Does your component have enough to get the role? Accessibility-first testing.
TypeScript
4
star
19

perforated

Pretty powerful forms set up simply using associated arrays or JSON, with automatic validation, dependencies and more.
PHP
3
star
20

swift-kick

Command line tool to ease Swift development
JavaScript
3
star
21

MERN-Boilerplate-Counter

Boilerplate for MongoDB + Express + React + Node
JavaScript
3
star
22

BurntFoundation

Pleasant Swift additions to Foundation
Swift
3
star
23

boxset

Work with sets and maps without friction in TypeScript
TypeScript
3
star
24

react-sow

Nested styles for React components
JavaScript
3
star
25

wasm-comparison

JavaScript
3
star
26

gazetteer

Isolate routing and data for better server rendering: supports React, Vue, Preact
TypeScript
3
star
27

react-cheat

Interactive cheatsheet for React
JavaScript
3
star
28

stir

Record time taken in your PHP website
PHP
2
star
29

Chassis

Swift
2
star
30

CollectedSwiftPlaygrounds

Swift Playgrounds for interacting with Collected.Press
Swift
2
star
31

Imbue

Color picker for iOS with Lab mode, wide color, adjustments
Swift
2
star
32

browser-workout

JavaScript
2
star
33

guilty-gulp

Gulp set up for browserify, coffeescript, compass (SCSS), SVG, with separate production and development folders.
JavaScript
2
star
34

renderToPipeableStreamPlayground

JavaScript
2
star
35

elm-preact

JavaScript
2
star
36

jest-zest

Shorter and more readable tests in jest
TypeScript
2
star
37

collected-source-appengine

Manage and process content, from Markdown to images
Go
2
star
38

react-meadow

Nested declarative forms in React, just add JSON
JavaScript
2
star
39

tela-site

Tela: Fundamental styles for typography, elements, layout, accessibility
JavaScript
2
star
40

mini_modules

A subscript of JavaScript modules in Elixir
Elixir
2
star
41

collected_live

Manage content and assets with immutable guarantees
Elixir
2
star
42

GLAViewController

NSViewController subclass for OS X 10.9+ for making set up and working with layout constraints easier.
Objective-C
2
star
43

Hercules

The web multiplied: view, search, and share in side-by-side browser panes
Swift
2
star
44

react-seeds

React stylers for nicer Flexbox + CSS
JavaScript
1
star
45

woodland

See the forest from the accessibility trees
TypeScript
1
star
46

pvlp

Base for creating a single page WordPress theme.
PHP
1
star
47

javascript-vs-ruby

JavaScript
1
star
48

yieldcss

CSS Components using JavaScript Generator Functions
TypeScript
1
star
49

itsybitsy

Map, filter, flat map, reduce — all in one place
TypeScript
1
star
50

scalemodel

Replicate a generator function’s code
TypeScript
1
star
51

movies-next-graphql

Movies app using Next.js, React, GraphQL
JavaScript
1
star
52

plz

A CLI for common developer queries
Go
1
star
53

GLAArrayEditing

Editable array for Cocoa/Touch models with convenient interface and friendly implementation
Objective-C
1
star
54

learn-by-building

TypeScript
1
star
55

react-hooks-examples

JavaScript
1
star
56

burntcaramel.com

JavaScript
1
star
57

BrandCase

A list of terms and brands with the correct casing
1
star
58

vercel-build-bonanza

HTML
1
star
59

icing-space-press

TypeScript
1
star
60

icing.space

1
star
61

Syrup

Asynchronous data flow in Swift using enums
Swift
1
star
62

collected-systems

Go
1
star
63

public-store

JavaScript
1
star
64

paredown

Simple subset of Markdown for writing HTML in Elixir.
Elixir
1
star
65

AirStay

Ruby
1
star
66

hello-phoenix

A demo Elixir/Phoenix app you can deploy to Skyliner.
Elixir
1
star
67

shoalmate

TypeScript
1
star
68

pivots

Readable discriminated unions for TypeScript
TypeScript
1
star
69

tela

CSS
1
star
70

recipes-app

TypeScript
1
star
71

Instarails

Ruby
1
star
72

GLAArray-Mantle

GLAArrayEditor store for Mantle objects with JSON loading & saving.
Objective-C
1
star