State Updater Module Functions

Published Last updated v3.0.0

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.


_40
function 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.


_41
function 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

  1. Avoid threading callbacks through our app as props
  2. 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.


_37
const addItem = (prevCart, item) => [...prevCart, item];
_37
_37
const 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
_37
function 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.


_37
const addItem = (item) => (prevCart) => [...prevCart, item];
_37
_37
const 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
_37
function 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 useReducers 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. Passing dispatch down like this is the recommended pattern for deep updates.

React Docs Hooks FAQ: How to Avoid Passing Callbacks Down

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.

Tweets by Dan Abramov

_44
function 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
_44
function 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

  1. Avoid threading callbacks through our app as props
  2. 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 useReducers 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. Passing dispatch down like this is the recommended pattern for deep updates.

React Docs Hooks FAQ: How to Avoid Passing Callbacks Down

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.

Tweets by Dan Abramov

_40
function 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 useCallbacks 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.

  1. The updater form like setCount(c => c + 1) conveys strictly less information than setCount(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. A Complete Guide to useEffect - Functional Updated & Google Docs

  2. The functional updater form can also help you to avoid dependencies for useEffect, useMemo or useCallback. Suppose you want to pass an increment function to a memoized child component. We can make sure the function doesn’t change too often with useCallback, but if we closure over count, we will still create a new reference whenever count changes. The functional updater avoids this problem altogether Things to know about useState - Bonus: Avoiding dependencies

  3. React guarantees that setState function identity is stable and won’t change on re-renders. This is why it’s safe to omit from the useEffect or useCallback dependency array. React Docs Hooks Reference: useState

  4. 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

  5. When setting a state variable depends on the current value of another state variable, you might want to try replacing them both with useReducer. When you find yourself writing setSomething(something => ...), it’s a good time to consider using a reducer instead. A Complete Guide to useEffect - Decoupling Updates from Actions