State Updater Module Functions
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. It takes a few steps so let’s make an inline callback for it.
_40function Store() {_40 const [cart, setCart] = useState([]);_40_40 // Adding an item is simple: Copy state into a new_40 // array, add the item and set it as new state_40 const addToCart = (item) => setCart([...cart, item]);_40_40 // Removing an item takes a bit more work_40 const removeFromCart = (item) => {_40 // Find the last added item of this type in the state array_40 const index = cart.lastIndexOf(item);_40_40 // Check if item is found in the cart_40 if (index !== -1) {_40 const newCart = [...cart]; // Copy state_40 newCart.splice(index, 1); // .splice() mutates the array_40 setCart(newCart);_40 }_40 };_40_40 return (_40 <div>_40 {items.map((item) => (_40 <div key={item}>_40 <span>{item}</span>_40 <button_40 onClick={() => addItem(item)}_40 >_40 +_40 </button>_40 <button_40 onClick={() => removeItem(item)}_40 >_40 -_40 </button>_40 </div>_40 ))}_40 </div>_40 );_40}
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 hook dep array 2.
_41function Store() {_41 const [cart, setCart] = useState([]);_41_41 // Adding an item is simple: Copy state into a new_41 // array, add the item and set it as new state_41 const addToCart = (item) => setCart((prevCart) => [...prevCart, item]);_41_41 // Removing an item takes a bit more work_41 const removeFromCart = (item) =>_41 setCart((prevCart) => {_41 const index = prevCart.lastIndexOf(item);_41_41 if (index !== -1) {_41 const newCart = [...prevCart];_41 newCart.splice(index, 1);_41 return newCart;_41 } else {_41 return prevCart;_41 }_41 });_41_41 return (_41 <div>_41 {items.map((item) => (_41 <div key={item}>_41 <span>{item}</span>_41 <button_41 onClick={() => addItem(item)}_41 >_41 +_41 </button>_41 <button_41 onClick={() => removeItem(item)}_41 >_41 -_41 </button>_41 </div>_41 ))}_41 </div>_41 );_41}
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.
_37const addItem = (prevCart, item) => [...prevCart, item];_37_37const removeItem = (prevCart, item) => {_37 const index = prevCart.lastIndexOf(item);_37_37 if (index !== -1) {_37 const newCart = [...prevCart];_37 newCart.splice(index, 1);_37 return newCart;_37 } else {_37 return prevCart;_37 }_37};_37_37function Store() {_37 const [cart, setCart] = useState([]);_37_37 return (_37 <div>_37 {items.map((item) => (_37 <div key={item}>_37 <span>{item}</span>_37 <button_37 onClick={() => setCart((prevCart) => addItem(prevCart, item))}_37 >_37 +_37 </button>_37 <button_37 onClick={() => setCart((prevCart) => removeItem(prevCart, item))}_37 >_37 -_37 </button>_37 </div>_37 ))}_37 </div>_37 );_37}
Instead of defining another inline function inside the state setter, you can ‘split the parameters’ in to two single-parameter functions. 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.
_37const addItem = (item) => (prevCart) => [...prevCart, item];_37_37const removeItem = (item) => (prevCart) => {_37 const index = prevCart.lastIndexOf(item);_37_37 if (index !== -1) {_37 const newCart = [...prevCart];_37 newCart.splice(index, 1);_37 return newCart;_37 } else {_37 return prevCart;_37 }_37};_37_37function Store() {_37 const [cart, setCart] = useState([]);_37_37 return (_37 <div>_37 {items.map((item) => (_37 <div key={item}>_37 <span>{item}</span>_37 <button_37 onClick={() => setCart(addItem(item))}_37 >_37 +_37 </button>_37 <button_37 onClick={() => setCart(removeItem(item))}_37 >_37 -_37 </button>_37 </div>_37 ))}_37 </div>_37 );_37}
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. Here, each state updater module function maps to an event in the reducer.
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.
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.
In large component trees, an alternative we recommend is to pass down a dispatch function from
useReducer
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.
And 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.
_44function cartReducer(prevCart, action) {_44 switch (action.type) {_44 case "add_item": {_44 return [...prevCart, action.item];_44 }_44 case "remove_item": {_44 const index = prevCart.lastIndexOf(action.item);_44_44 if (index !== -1) {_44 const newCart = [...prevCart];_44 newCart.splice(index, 1);_44 return newCart;_44 } else {_44 return prevCart;_44 }_44 }_44 default:_44 return prevCart;_44 }_44}_44_44function Store() {_44 const [cart, dispatch] = useReducer(cartReducer,[]);_44_44 return (_44 <div>_44 {items.map((item) => (_44 <div key={item}>_44 <span>{item}</span>_44 <button_44 onClick={() => dispatch({ type: "add_item", item })}_44 >_44 +_44 </button>_44 <button_44 onClick={() => dispatch({ type: "remove_item", item })}_44 >_44 -_44 </button>_44 </div>_44 ))}_44 </div>_44 );_44}
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. It takes a few steps so let’s 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 hook dep 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. 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. Here, each state updater module function maps to an event in the reducer.
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.
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.
In large component trees, an alternative we recommend is to pass down a dispatch function from
useReducer
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.
And 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.
_40function Store() {_40 const [cart, setCart] = useState([]);_40_40 // Adding an item is simple: Copy state into a new_40 // array, add the item and set it as new state_40 const addToCart = (item) => setCart([...cart, item]);_40_40 // Removing an item takes a bit more work_40 const removeFromCart = (item) => {_40 // Find the last added item of this type in the state array_40 const index = cart.lastIndexOf(item);_40_40 // Check if item is found in the cart_40 if (index !== -1) {_40 const newCart = [...cart]; // Copy state_40 newCart.splice(index, 1); // .splice() mutates the array_40 setCart(newCart);_40 }_40 };_40_40 return (_40 <div>_40 {items.map((item) => (_40 <div key={item}>_40 <span>{item}</span>_40 <button_40 onClick={() => addItem(item)}_40 >_40 +_40 </button>_40 <button_40 onClick={() => removeItem(item)}_40 >_40 -_40 </button>_40 </div>_40 ))}_40 </div>_40 );_40}
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.
Context Module Functions is similar to what I described above but Context Module Functions solve it from the other end.
Move the helper functions outside of React, import them where needed (deep down in leaf components) and pass dispatch
to the helper functions.
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 with all the 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.