At Input Logic, we use atom to manange state in our React apps. atom
is modeled after Redux, which is a pattern for state management that has stood up in countless apps over the years. This is a succinct overview of the atom/redux concepts and best practices we employ.
The three principles are the foundation of Redux. Briefly:
- Your application (or shared) state is stored in a single object.
- The application state object is immutable. You cannot just
globalState.someValue = 4
as the application state object is not directly accessible. - Changes are made with pure functions. You define actions and reducers to describe and handle changes to your application state.
The first two prinicples are rather self-explanatory, but actions and reducers are a new concept. To change your state object, you need to dispatch
an action.
An action can be just a string, or an object or any value you want. However, we enforce using Flux Standard Actions. So, an action is an object that describes a state change.
store.dispatch({ type: 'INCREMENT' })
To handle those state change descriptions, we define reducers. A reducer is a function that accepts function (action, state)
and returns the potentially modified state. There is no limit to the number of reducers you define. When an action is dispatched, atom
will call each defined reducer, in the given order. Each subsequent reducer receives the potentially modified state from the previous reducer.
// atom accepts a single reducer or an array.
// `0` is our initial state.
const store = atom(counter, 0)
function counter (action, state) {
switch (action.type) {
case 'INCREMENT':
return state + 1
case 'DECREMENT':
return state - 1
default:
return state
}
}
Why not just have a single object that can be mutated? Why not just have a setState
function on a global store, instead of creating actions and reducers? I've written about this in detail, but briefly:
- actions and reducers allow traceable state changes.
- Refactoring is much easier.
Not sure when to use useState
in your React component versus global state? We follow this pretty basic rule:
If your data is shared in more than one Component, store it in global state.
I actually found Redux could be made even simpler. Some concepts are removed, and some conveniences are added:
- Redux supports a single Reducer, providing a combineReducers function to help break apart your reducer logic.
atom
accepts an array of reducers, and actually reduces them. - Because
atom
supports an array of reducers, it provides theaddReducer
andremoveReducer
functions instead of Redux'sreplaceReducer
. atom
does not implement middleware. Middleware is considered dangerous. It can interfere with traceability and can very easily complicate code. Instead,atom
has a very primitive listener api, and thunks are built in.atom
comes withsetState
, utilizing an internal reducer that will merge the passed in state with the existing state, for faster prototyping.
To keep the global state object manageable, we recommend limiting the amount of nested objects and arrays. Generally, you don't want to nest more than 1 level deep on the global state object.
// bad 👎
{
blog: {
posts: []
}
}
// good 👍
{
blogPosts: []
}
TODO
TODO
Hopefully, that covers everything you need to know about atom
/redux and global state. If you have any questions, please reach out!