State Updater Module Functions

In React, you can reuse state updater functions by moving them to module scope.

Published · Last updated

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 , which can remove a dependency for a dependency array .


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

  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 .

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 .

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 . It’s considered more idiomatic React, passing down useReducers 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 , which can remove a dependency for a dependency array .

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 .

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 .

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 . It’s considered more idiomatic React, passing down useReducers 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 . 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 showing all the different 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
  6. 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