Utility Actions
Generic setter
Complex actions often need to call multiple child actions.
const actions = {
validate: _ => state => ({valid: validationCheck(state)}),
_setFoo: x => ({foo: x}),
_setBar: x => ({bar: x}),
setFoo: x => (state, actions) => {
actions._setFoo(x)
actions.validate()
},
setBar: x => (state, actions) => {
actions._setBar(x)
actions.validate()
},
}
The reason code ends up looking like the above, is because an action can only update the state by returning a partial state. So unless you want to repeat your validation code a lot of places, the set...
actions need to use a separate action to set the prop, and then call the validation action.
The above could be made much simpler with a setter:
set: x => x
That way, the extra _set...
can be replaced simply with:
const actions = {
set: x => x,
validate: _ => state => ({valid: validationCheck(state)}),
setFoo: x => (state, actions) => {
actions.set({foo: x})
actions.validate()
},
setBar: x => (state, actions) => {
actions.set({bar: x})
actions.validate()
}
}
Generic reducer
The set
functions like above are also useful in actions that perform async operations such
as fetch:
fetchData: id => (state, actions) => {
fetch(`http://example.com/data/${id}`)
.then(res => res.json())
.then(data => actions.set({data}))
}
...but what if the resulting data we wish to set depends on the current state?
We might try:
fetchData: id => (state, actions) => {
fetch(`http://example.com/data/${id}`)
.then(res => res.json())
.then(data => actions.set({
data: state.data + data
}))
}
...but because the fetch is async, the state we have may be old! Setting the result in this way would overwrite any changes to state.data
that happened while we were
waiting for the request to finish.
Since you alway get the latest state when you call an action, the simple solution is
not to set data directly in .then()
but to set it in an action:
didFetchData: data => state => ({data: state.data + data}),
fetchData: id => (state, actions) => {
fetch(`http://example.com/data/${id}`)
.then(res => res.json())
.then(actions.didFetchData)
}
However this means you need to create a didFetchFoo
action to correspond with every
fetchFoo
.
A more ergonomical solution would be to use a generic reducer action such as this:
reduce: fn => state => fn(state),
fetchData: id => (state, actions) => {
fetch(`http://example.com/data/${id}`)
.then(res => res.json())
.then(data => actions.reduce(state => {
data: state.data + data
}))
}
Incidentally, a generic reducer could also be used to simplify the non-async validation example above, as such:
const actions = {
set: x => x,
reduce: fn => state => fn(state)
validate: props => (state, actions) => {
actions.set(props)
actions.reduce(state => {
valid: validationCheck(state)
})
}
setFoo: x => (state, actions) => actions.validate({foo: x})
setBar: x => (state, actions) => actions.validate({bar: x})
}
(wether or not that's really simpler is subjective, perhaps)
The problem of scope
The annoying thing about using utility functions such as these, are that actions are only able to call actions from within their branch of the action tree. So you can't make single set
and reduce
actions at the top of the tree and hope to call them from any action lower down. You need to replicate them at every level you want to use them.
This is an example of the more general problem of composing actions from different branches of the action tree (like: "when x happens here, that makes y happen over there"). There should be some nice simple solution to it, but I haven't found it yet. I will be exploring this more in the future.