react-organism v0.3.9
React Organism
Dead simple React/Preact state management to bring pure components alive
- Supports
async/awaitand easy loading (e.g.fetch()) - Reload when particular props change
- Animate using generator functions: just
yieldthe new state for each frame - Tiny: 1.69 KB gzipped (3.49 KB uncompressed)
- Embraces the existing functional
setStatewhile avoiding boilerplate (no writingthis.setState()or.bindagain) - Easy to unit test
Table of contents
Installation
npm i react-organism --saveDemos
- Animated counter
- Dynamic loading with
import() - Live form error validation with Yup
- Multicelled component — using multiple states
- Todo List
- Inputs, forms, animation, fetch · code
- User Stories Maker
- React Cheat Sheet · code
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
handlerswhich correspond to each function inStateFunctionsand are ready to be passed to e.g.onClick,onChange, etc.loadError?: Error produced by theloadhandlerhandlerError?: 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 theasync/awaitsyntax. - An iterator, such as one made by using a generator function. Each object passed to
yieldmay 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, andnamefromevent.target. Additionally, if target hasdata-attributes, these will also be extracted in camelCase from itsdataset. Suffixingdata-attributes with_numberwill convert value to a number (instead of string) usingparseFloat, and drop the suffix. Handler will receive these extracted values in an object as the first argument, followed by the original arguments. - For
submitevents, extracts values of<input>fields in a<form>. Handler will receive the values keyed by the each input’snameattribute, followed by the original arguments. Pass the handler to theonSubmitprop of the<form>. Form must havedata-extractattribute present. To clear the form after submit, adddata-resetto 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
asyncandawaitin any action - Supports generator functions to allow multiple state changes — great for animation
- No
switchstatements - No boilerplate or additional helper libraries needed
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago
8 years ago


