State Updater Module Functions
In React, you can reuse state updater functions by moving them to module scope.
In React, state is immutable. This idea may seem incompatible with mutable structures like arrays or objects. We can place those in state too, as long as we treat them as though they’re immutable.
To update an array in state, copy it first, then update it. This takes a few steps so we’ll make an inline callback for it.
function Store() { const [cart, setCart] = useState([]); // Adding an item is simple: Copy state into a new // array, add the item and set it as new state const addToCart = (item) => setCart([...cart, item]); // Removing an item takes a bit more work const removeFromCart = (item) => { // Find the last added item of this type in the state array const index = cart.lastIndexOf(item); // Check if item is found in the cart if (index !== -1) { const newCart = [...cart]; // Copy state newCart.splice(index, 1); // .splice() mutates the array setCart(newCart); } }; return ( <div> {items.map((item) => ( <div key={item}> <span>{item}</span> <button onClick={() => addItem(item)} > + </button> <button onClick={() => removeItem(item)} > - </button> </div> ))} </div> );}
Besides a new value, you can also pass a function to a state-setter. It receives the prior state, and describes how to update it. It’s neat and comes in handy to when having to base new state on prior state. This is particularly useful for updating mutable structures. Another benefit is that you don’t tie the update to the current state value from scope 1, which can remove a dependency for a dependency array 2.
function Store() { const [cart, setCart] = useState([]); // Adding an item is simple: Copy state into a new // array, add the item and set it as new state const addToCart = (item) => setCart((prevCart) => [...prevCart, item]); // Removing an item takes a bit more work const removeFromCart = (item) => setCart((prevCart) => { const index = prevCart.lastIndexOf(item); if (index !== -1) { const newCart = [...prevCart]; newCart.splice(index, 1); return newCart; } else { return prevCart; } }); return ( <div> {items.map((item) => ( <div key={item}> <span>{item}</span> <button onClick={() => addItem(item)} > + </button> <button onClick={() => removeItem(item)} > - </button> </div> ))} </div> );}
If you find yourself repeating a state updater, you could pass the callback around as props.
You can also decouple the logic from the component. Instead of putting the state updating logic in inline callbacks, extract it to functions outside of React and call them inside the state updater function.
You can export these functions and conveniently import them where needed.
Two has two benefits
- Avoid threading callbacks through our app as props
- Avoid having to wrap them in
useCallback
for use in hooks. The state-setter is guaranteed to be stable 3.
It’s a state updater function in module scope, so I call it a State updater module function.
const addItem = (prevCart, item) => [...prevCart, item];const removeItem = (prevCart, item) => { const index = prevCart.lastIndexOf(item); if (index !== -1) { const newCart = [...prevCart]; newCart.splice(index, 1); return newCart; } else { return prevCart; }};function Store() { const [cart, setCart] = useState([]); return ( <div> {items.map((item) => ( <div key={item}> <span>{item}</span> <button onClick={() => setCart((prevCart) => addItem(prevCart, item))} > + </button> <button onClick={() => setCart((prevCart) => removeItem(prevCart, item))} > - </button> </div> ))} </div> );}
Instead of defining another inline function inside the state-setter, you can ‘split the parameters’ in to two single-parameter functions (also know as currying). The first function takes the item and returns a state updater function (a function that takes the previous state).
I really like this. It’s just a bit more concise and just as readable. But that’s just me, I know not everyone likes higher-order function and that’s OK.
const addItem = (item) => (prevCart) => [...prevCart, item];const removeItem = (item) => (prevCart) => { const index = prevCart.lastIndexOf(item); if (index !== -1) { const newCart = [...prevCart]; newCart.splice(index, 1); return newCart; } else { return prevCart; }};function Store() { const [cart, setCart] = useState([]); return ( <div> {items.map((item) => ( <div key={item}> <span>{item}</span> <button onClick={() => setCart(addItem(item))} > + </button> <button onClick={() => setCart(removeItem(item))} > - </button> </div> ))} </div> );}
All this may remind you of that other, more daunting, stateful hook useReducer
.
useReducer
is conceptually a little different.
It’s about sending actions — commands that instruct how to update state — rather than directly setting state.
All state updating logic is consolidated into a single reducer function. A reducer constructs new state based on prior state in response to actions 4.
A reducer decouples state updating logic from components by encapsulating it. Decoupling helps to avoid common pitfalls with hooks, like stale closures or hooks firing too often.
Here, each state updater module function maps to an event in the reducer.
Usually, heavy use of function updaters is a sign to consider replacing useState
with useReducer
5.
It’s considered more idiomatic React, passing down useReducer
s dispatch
is the recommended way to pass less callbacks down in the React Docs.
Here’s React Core team member Dan on why a dispatch
function is better than callbacks:
Dispatch is better than helper methods. Helper methods are object junk that we need to recreate and compare for no purpose other than superficially nicer looking syntax. There’s no reason to literally pass more things down when you could’ve passed dispatch alone, and then use it directly (or pass it to more things). Passing raw dispatch down also makes code splitting such "async methods" easier. You import them from the leaf component that uses them instead of the root component. So you only pay for what you use where you use it.
function cartReducer(prevCart, action) { switch (action.type) { case "add_item": { return [...prevCart, action.item]; } case "remove_item": { const index = prevCart.lastIndexOf(action.item); if (index !== -1) { const newCart = [...prevCart]; newCart.splice(index, 1); return newCart; } else { return prevCart; } } default: return prevCart; }}function Store() { const [cart, dispatch] = useReducer(cartReducer,[]); return ( <div> {items.map((item) => ( <div key={item}> <span>{item}</span> <button onClick={() => dispatch({ type: "add_item", item })} > + </button> <button onClick={() => dispatch({ type: "remove_item", item })} > - </button> </div> ))} </div> );}
In React, state is immutable. This idea may seem incompatible with mutable structures like arrays or objects. We can place those in state too, as long as we treat them as though they’re immutable.
To update an array in state, copy it first, then update it. This takes a few steps so we’ll make an inline callback for it.
Besides a new value, you can also pass a function to a state-setter. It receives the prior state, and describes how to update it. It’s neat and comes in handy to when having to base new state on prior state. This is particularly useful for updating mutable structures. Another benefit is that you don’t tie the update to the current state value from scope 1, which can remove a dependency for a dependency array 2.
If you find yourself repeating a state updater, you could pass the callback around as props.
You can also decouple the logic from the component. Instead of putting the state updating logic in inline callbacks, extract it to functions outside of React and call them inside the state updater function.
You can export these functions and conveniently import them where needed.
Two has two benefits
- Avoid threading callbacks through our app as props
- Avoid having to wrap them in
useCallback
for use in hooks. The state-setter is guaranteed to be stable 3.
It’s a state updater function in module scope, so I call it a State updater module function.
Instead of defining another inline function inside the state-setter, you can ‘split the parameters’ in to two single-parameter functions (also know as currying). The first function takes the item and returns a state updater function (a function that takes the previous state).
I really like this. It’s just a bit more concise and just as readable. But that’s just me, I know not everyone likes higher-order function and that’s OK.
All this may remind you of that other, more daunting, stateful hook useReducer
.
useReducer
is conceptually a little different.
It’s about sending actions — commands that instruct how to update state — rather than directly setting state.
All state updating logic is consolidated into a single reducer function. A reducer constructs new state based on prior state in response to actions 4.
A reducer decouples state updating logic from components by encapsulating it. Decoupling helps to avoid common pitfalls with hooks, like stale closures or hooks firing too often.
Here, each state updater module function maps to an event in the reducer.
Usually, heavy use of function updaters is a sign to consider replacing useState
with useReducer
5.
It’s considered more idiomatic React, passing down useReducer
s dispatch
is the recommended way to pass less callbacks down in the React Docs.
Here’s React Core team member Dan on why a dispatch
function is better than callbacks:
Dispatch is better than helper methods. Helper methods are object junk that we need to recreate and compare for no purpose other than superficially nicer looking syntax. There’s no reason to literally pass more things down when you could’ve passed dispatch alone, and then use it directly (or pass it to more things). Passing raw dispatch down also makes code splitting such "async methods" easier. You import them from the leaf component that uses them instead of the root component. So you only pay for what you use where you use it.
function Store() { const [cart, setCart] = useState([]); // Adding an item is simple: Copy state into a new // array, add the item and set it as new state const addToCart = (item) => setCart([...cart, item]); // Removing an item takes a bit more work const removeFromCart = (item) => { // Find the last added item of this type in the state array const index = cart.lastIndexOf(item); // Check if item is found in the cart if (index !== -1) { const newCart = [...cart]; // Copy state newCart.splice(index, 1); // .splice() mutates the array setCart(newCart); } }; return ( <div> {items.map((item) => ( <div key={item}> <span>{item}</span> <button onClick={() => addItem(item)} > + </button> <button onClick={() => removeItem(item)} > - </button> </div> ))} </div> );}
Context Module Functions
useReducer
can also suffer from the curse of convenience callbacks.
If you make ‘helper functions’ around dispatch
, you’d still end up have to wrap them in useCallback
.
Context Module Functions is a pattern to fix that. To cite the oneliner from EpicReact Advanced React Patterns:
Context Module Functions allows you to encapsulate a complex set of state changes into a utility function which can be tree-shaken and lazily loaded.
The idea behind ‘Context Module Functions’ is similar to what I described above but Context Module Functions solve it from the other end.
Here, the helper functions are moved outside of React, and imported where needed in , and dispatch
is passed down to the helper functions 6.
dispatch
is always stable so it’s better to pass down than multiple callbacks.
Memoizing multiple callbacks in a custom hook
If you’re making a custom hook with multiple wrapped state-setters and returning them in ‘handlers object’ like the typical [state, handlersObject]
return signature,
it’s good to wrap handlersObject
in a useMemo
so it is stable.
Why? Because when you wrap all the wrapped state-setters in useCallback
and return those in an object,
it will still be a brand new object each render!
This is easily overlooked, you’d have to wrap that object in a useMemo
too, with all the callbacks in its dependency array.
But why bother with multiple useCallback
s and a useMemo
when you can just memoize the ‘final’ handlers object with single useMemo
and achieve the same? Much simpler!
Credits to Kyle Shevlin for this blogpost: useMemo
and Stable Values
Here’s a CodeSandbox showing all the different approaches.
- An excellent example of ‘State Updater Module Functions’ to handle an object in state can be found in Steve Ruiz’s blogpost Creating a Zoom UI (CodeSandbox).
- A Complete Guide to useEffect by Dan Abramov. I highly recommend reading this.
It’s long but and there’s so much good stuff in it. It’s about more than just
useEffect
, it contains good lessons on updating state and React’s design in general. - Hooks, State, Closures, and
useReducer
by Adam Rackis. - Should I
useState
oruseReducer
by Kent C. Dodds.
-
↩
The updater form like
A Complete Guide to useEffect - Functional Updated & Google DocssetCount(c => c + 1)
conveys strictly less information thansetCount(count + 1)
because it isn’t “tainted” by the current count. It only expresses the action (“incrementing”). Thinking in React involves finding the minimal state. This is the same principle, but for updates. -
↩
The functional updater form can also help you to avoid dependencies for
Things to know about useState - Bonus: Avoiding dependenciesuseEffect
,useMemo
oruseCallback
. Suppose you want to pass an increment function to a memoized child component. We can make sure the function doesn’t change too often withuseCallback
, but if we closure overcount
, we will still create a new reference whenever count changes. The functional updater avoids this problem altogether -
↩
React guarantees that
React Docs Hooks Reference: useStatesetState
function identity is stable and won’t change on re-renders. This is why it’s safe to omit from theuseEffect
oruseCallback
dependency array. -
↩
Instead of reading the state inside an effect, it dispatches an action that encodes the information about what happened. This allows our effect to stay decoupled from the step state. Our effect doesn’t care how we update the state, it just tells us about what happened. And the reducer centralizes the update logic.
A Complete Guide to useEffect - Decoupling Updates from Actions -
↩
When setting a state variable depends on the current value of another state variable, you might want to try replacing them both with
A Complete Guide to useEffect - Decoupling Updates from ActionsuseReducer
. When you find yourself writingsetSomething(something => ...)
, it’s a good time to consider using a reducer instead. -
↩
In large component trees, an alternative we recommend is to pass down a dispatch function from
React Docs Hooks FAQ: How to Avoid Passing Callbacks DownuseReducer
via context: This is both more convenient from the maintenance perspective (no need to keep forwarding callbacks), and avoids the callback problem altogether. Passingdispatch
down like this is the recommended pattern for deep updates.