Dead simple React/Preact state management to bring pure components alive
async
/await
and easy loading (e.g. fetch()
)yield
the new state for each framesetState
while avoiding boilerplate (no writing this.setState()
or .bind
again)npm i react-organism --save
import()
// 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>
)
}
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 } />
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>
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 })
})
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
}
}
})
data-
attributes and <forms>
Example coming soon
// 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))
}
})
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>
)
}
}
Example coming soon.
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:
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
handlerhandlerError?
: Error produced by any other handlerStateFunctions
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 export
ed 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).
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 argumentsHandlers must return one of the following:
setState(changes)
.setState((prevState, props) => changes)
.handlerError
. Alternatively, your handler can use the async
/await
syntax.yield
may be one of the above (object / function / promise).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
.
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:
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.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.import()
encourages breaking apps into sectionsasync
and await
in any actionswitch
statements