State Updater Module Functions

Published Last updated

State drives all interactivity in React apps. It’s a big deal and you’ll deal with state updating logic a lot in React: processing keyboard input, tracking pointer coordinates, adding and removing strings to an array, you name it. It can get pretty involved. Often this logic is organized in callback functions in components, helper functions that handle state updating logic and wrap the state setter. These can be passed around as props and called in event handlers or useEffect`s. Let’s name these wrapped state setters. These work but there are different – sometimes better – ways to approach it.

So in the spirit of show code and tell, let’s go over several different ways to organize state updating logic, starting with wrapped state setters and ending at useReducer while introducing a pattern in between.

In short, the main idea is that if an update is expressed in terms of prior state, it should be done with state updater function or a reducer instead of a wrapped state setter. The in-between pattern introduces in this post is naming and moving state updater functions out of React, that's it. I'll explain it in more detail later, but first let us review a basic principle of React state.

In React, state is read-only, , ever. We can only set it to a new value 1. This idea may seem incompatible with mutable structures like arrays or objects. We can place those in state too, we should just treat them as though they’re immutable, even though they really aren’t.

TypeScript Tip for useState

Be warned about accidental mutation of arrays in state by adding readonly to the type annotation. Use utility type ReadOnly<ObjectType> for objects in state. Note that ReadOnly<> .

Often we want base new state on state. Because state should always replaced and never mutated, a new copy with the desired changes should be made, and then that should be set it as new state.

I noticed wrapped state setters are frequently made for handling . Copying and modifying an array or object gets verbose and repetitive fast, so naturally, we extract it to a state callback, and than pass that around.

To make it more concrete; suppose we have a shopping cart in state: an array with the names of selected products. Let’s make some callbacks to add and remove items:

import { useState } from "react";

const items = ["Cat food🐱", "Dog food🐶", "Fish food🐟", "Rabbit food🐰"];

export default function PetFoodStore() {
  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 className="App">
      <h1>Pet Food Store</h1>
      <h2>Cart</h2>
      <div>{cart.length ? cart.join(", ") : "Nothing in cart..."}</div>
      <h2>Items</h2>
      <Inventory addToCart={addToCart} removeFromCart={removeFromCart} />
    </div>
  );
}

function Inventory({ addToCart, removeFromCart }) {
  return items.map((item) => (
    <div key={item}>
      <span>{item}</span>
      <button onClick={() => addToCart(item)}>+</button>
      <button onClick={() => removeFromCart(item)}>-</button>
    </div>
  ));
}

The addToCart and removeFromCart callbacks are . Most of the time this is fine 2 but there are some cases where unstable function identities may become an issue, such as:

  • If the functions are called in another hook and need to be in its dependency array, it will cause the hook to run everytime the component rerenders 3.
  • If the functions are defined high up in the component hierarchy with an expensive component subtree, say in a Context provider high in you 4.
  • If the functions are passed to a memoized component they will break memoization.

In any of these cases, just wrap them in useCallback so they’re referentially stable.

  ...

  const addToCart = useCallback((item) => setCart([...cart, item]), [cart]);

  const removeFromCart = useCallback(
    (item) => {
      const index = cart.lastIndexOf(item);

      if (index !== -1) {
        const newCart = [...cart];
        newCart.splice(index, 1);
        setCart(newCart);
      }
    },
    [cart]
  );

  ...

We could even move the state and helpers functions into a custom hook, read the note below for an important caveat on doing that.

⚠ Note on 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

Notice that we’re reading the cart state variable from component scope in the wrapped state setters? Therefore, it has to be included in the dependency array of useCallback which will make the function reference change everytime it’s called, kind of defeating the purpose of useCallback.

We can sidestep reading the state from component scope and ensure stability by using a ; a function that is passed to the state setter and transforms the previous state 5 6.

With a state updater function, the new state value is determined inside the state setter rather than in a function around it. I view it as working “from the inside out” rather than “from the outside in” if that makes sense (does it?).

More on state updater functions

Instead of a new value, a function can be passed to the state setter; a 'state updater function'. This function receives the previous state as its only parameter and returns a new state.

You can think of it as “sending an instruction” to React about how the state should change

A Complete Guide to useEffect - Making Effects Self-Sufficient by Dan Abramov

By convention, state updaters are written as anonymous inline arrow functions - setCount((prevCount) => prevCount + 1) - just like the functions passed to useEffect or useMemo.

As we’ve just seen, state updater functions come in handy when accessing the state variable is inconvenient. State updater function enable an additional benefit, namely ’queuing’ of multiple state update, which ensure updates build on top of each other instead of on the state from component scope.

Any time I need to compute new state based on previous state, I use a function update.

useState lazy initialization and function updates

by Kent C. Dodds

  ...

  const addToCart = useCallback(
    (item) => setCart((prevCart) => [...prevCart, item]),
    []
  );

If the given item to removeItem is not found in state, the previous state is returned and 7 8.

  const removeFromCart = useCallback((item) => {
    setCart((prevCart) => {
      const index = prevCart.lastIndexOf(item);
      if (index !== -1) {
        const newCart = [...prevCart];
        newCart.splice(index, 1);
        return newCart;
      } else {
        // If the item is not found, return the previous state
        return prevCart
      }
    });
  }, []);

  ...

That’s already nicer but I still think we can do better. It still feels like a lot of ceremony for a simple state update.

You may choose to drop the addItem and removeItem callbacks altogether and call the state setter with updater function (setCart(prevCart => ...)) directly in the event handler. This way we avoid the ritual of wrapping them in useCallback and then threading them through our app as props. On top of that, useCallback doesn't come for free either 9

function Inventory({ setCart }) {
  return items.map((item) => (
    <div key={item}>
      <button onClick={() => setCart((prevCart) => [...prevCart, item])}>
        + {item}
      </button>
      <button
        onClick={() =>
          setCart((prevCart) => {
            const index = prevCart.lastIndexOf(item);
            if (index !== -1) {
              const newCart = [...prevCart];
              newCart.splice(index, 1);
              return newCart;
            }
          })
        }
      >
        - {item}
      </button>
    </div>
  ));
}

And this is fine if this is the only place where we’re updating cart like this. What if we are adding and removing items from cart in multiple (distant) components though? Do we repeat the state updater function everywhere? That gets tedious fast, moreover DRY!

How do we share the state updating logic between event handlers? Should we have stuck with wrapped state setters? Is it time to break out useReducer? A state management lib perhaps? Hold on, there’s a way to keep the number of props down, not have to worry about stable props, and not even have to useCallback.

These functions (addToCart & removeFromCart) only update cart state, they don’t do anything else like make network requests or set some other state. Everything done by these wrapped state setters can be done in a state updater function just as well.

So what if we extract a state updater to its own named function outside of React? It’s a pure function, it only needs the previous state and the selected item, so we can move it out just fine. We can even place it in a seperate file, a module, and conveniently import it where needed. Hence the name: ”State Updater Module Functions”

state-updaters.jsexport const addItem = (prevCart, item) => [...prevCart, item];

export const removeItem = (prevCart, item) => {
  const index = prevCart.lastIndexOf(item);

  if (index !== -1) {
    const newCart = [...prevCart];
    newCart.splice(index, 1);
    return newCart;
  } else {
    return prevCart;
  }
};

If we pass setCart and import the state updater functions:

import { addItem, removeItem } from "../state-updaters.js";

function Inventory({ setCart }) {
  return 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>
  ));
}

This has the happy consequence that we have to pass just one prop for updating state, setCart itself, which is 11. We can use the same updater function in different places so we don’t repeat ourselves.

It’s already a lot better with the updater functions placed outside, but the state updater function only takes a single parameter: the previous state. That’s why I think defining another inline function inside setCart is unnecessary. Let's get more functional by splitting the function in to two single-parameter functions. The first function takes the item and returns another function that takes the previous state; a state updater function. For these , I prefer to use arrow syntax:

state-updaters.jsexport const addItem = (newItem) => (prevCart) => [...prevCart, newItem];

export const removeItem = (removeItem) => (prevCart) => prevCart.includes(removeItem)
    ? [...prevCart].filter((item) => item !== removeItem)
    : prevCart;

And in the event handlers, we can call setCart with the appropriate state updater module function with the item as its argument:

import { addItem, removeItem } from "./state-updaters.js";

function Inventory({ setCart }) {
  return items.map((item) => (
    <div key={item}>
      <span>{item}</span>
      <button
        title={`Add one ${item} to cart`}
        onClick={() => setCart(addItem(item))}
      >
        +
      </button>
      <button
        title={`Remove one ${item} from cart`}
        onClick={() => setCart(removeItem(item))}
      >
        -
      </button>
    </div>
  ));
}

I find this really clear in intent, and both more concise and more ergonomic than wrapped state setters. I must note that writing higher-order functions is just my preference, if you think it looks weird you’re free to use anonymous inline functions instead.

I especially like this pattern when dealing with arrays or objects in state, though it’s not limited to that. To use the classic ’counter state’ as an example, here’s what an increment state updater would look like:

const increment = (add) => (prevCount) => prevCount + add;
const decrement = (subtract) => (prevCount) => prevCount - subtract;

... 

  <button onClick={() => setCount(increment(number))}>Add {number}</button>

But I think prevCount + add is simple enough on its own and doesn’t even really need an abstraction. I’d just use an inline state updater here.

All this may remind you of that other, more daunting stateful hook useReducer.

useReducer is conceptually a little different than useState. 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 that constructs new state in response to actions, 12. A reducer decouples state updating logic from components by encapsulating it, it's a more mapped-out model of state. The decoupling helps in avoiding common pitfalls with hooks, like stale closures or hooks firing too often.

You can view a state updater function as a sort of mini-reducer, it also instructs how to change state. It’s kind of like one separate action in a reducer. I like to think of them on a scale where updating logic is gradually placed closer to the stateful hook itself. To use toggling of boolean state as an example:

  1. Plain useState with state from component scope (f.e. in callbacks): setIsOpen(!isOpen)
  2. useState with a state updater function: setIsOpen((isOpen) => !isOpen)
  3. Centralized in a reducer: const [isOpen, toggle] = useReducer((isOpen) => !isOpen, false)

State updater functions are much more limited compared to useReducer. For one, useReducer is more equipped to handle dependant states, i.e. when some state consists of sub-states than depend on each other.

Second, useReducer has the powerful trait that it has access to when the reducer is placed in the component, because React will call it in the next render 13 14 . This is not possible with state updater functions, or any 3rd pary state management solution for that matter 15 16.

TypeScript Tip for useReducer

Type your reducer actions and states as discriminated unions and get helpful warnings and that sweet autocomplete in your editor. Ben Ilegbodu has more on this: Type-checking React useReducer in TypeScript

Usually, heavy use of function updaters is a sign to consider replacing useState with useReducer 17. It’s more idiomatic React, passing down useReducers dispatch is the recommended way to pass less callbacks down in the React Docs. State Updater Module Functions is a step in that direction, but still with useState.

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.

[and]

We recommend to pass dispatch down in context rather than individual callbacks in props.

React Docs Hooks FAQ: How to Avoid Passing Callbacks Down

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
Context Module Functions

useReducer can also suffer from the curse of convenience callbacks. If you make ‘helper functions’ around dispatches, you’d still end up wrapping 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 solve it from the other end, we 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.

If updating some state in terms of its current/previous value is all that a callback in a component does, you’re probably better off using a state updater or a reducer instead. A state callback ties state updating logic to a component and its render scope, while it need not be.

If you like callbacks because they're named and reusable, consider that you can move the state updater to a file and export it to make it named and reusable too. That’s what I call ’State Updater Module Functions’.

It may seem unusual and I wouldn’t recommend using it all the time, I think there’s a place for it though. It can not only help reduce props, but also help avoid useCallback.

I like it to use it when dealing with arrays and objects in useState. Decide for yourself, it's just another tool in your vanilla React state-management belt.

Heres a CodeSandbox that brings all code blocks in this post to life. Dig in to code, compare, and click around to verify all 8 approaches work the same!

If you have comments or feedback, I haven’t added comment functionality here yet so your best bet is to hit me up on Twitter.

  1. React docs: State as a Snapshot

  2. No. In modern browsers, the raw performance of closures compared to classes doesn’t differ significantly except in extreme scenarios. React Docs Hooks FAQ: Are hooks slow because of creating functions in render?

  3. However, when you are creating a reusable abstraction such as a custom hook, returning unstable values can be potentially dangerous. You don’t know if ultimately the users of your hook/API/library would end up putting any unstable values in a dependency array. Preemptive Memoization In React Is Probably Not Evil (Yet)

  4. Here, the context value is a JavaScript object with two properties, one of which is a function. Whenever MyApp re-renders, this will be a different object pointing at a different function, so React will also have to re-render all components deep in the tree that call useContext(...). In smaller apps, this is not a problem. However, there is no need to re-render them if the underlying data, like currentUser, has not changed. To help React take advantage of that fact, you may wrap the login function with useCallback and wrap the object creation into useMemo. This is a performance optimization. React Docs useContext: Optimizing re-renders when passing objects and functions

  5. When we want to update state based on the previous state, we can use the functional updater form of setState: A Complete Guide to useEffect - Making Effects Self-Sufficient

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

  7. If you update a State Hook to the same value as the current state, React will bail out without rendering the children or firing effects. Hooks reference: Bailing out of a state update

  8. React will ignore your update if the next state is equal to the previous state, as determined by an Object.is comparison useState docs: I’ve updated the state, but the screen doesn’t update

  9. Shallow comparisons aren’t free. They’re O(prop count). And they only buy something if it bails out. All comparisons where we end up re-rendering are wasted. Why would you expect always comparing to be faster? Considering many components always get different props. Tweet by Dan Abramov

  10. We’ve found that most people don’t enjoy manually passing callbacks through every level of a component tree. Even though it is more explicit, it can feel like a lot of ’plumbing’. Hooks FAQ: How to avoid passing callbacks down?

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

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

  13. Another great trait of reducers is that you can inline them, or closure over props. This comes in very handy if you need access to props or server state (e.g. coming from a useQuery hook) inside your reducer. Instead of "copying" these things into the reducer by using the state initializer, you can pass it to a function: useState vs useReducer - Passing props to reducers

  14. But what if we need props to calculate the next state? For example, maybe our API is <Counter step={1} />. Surely, in this case we can’t avoid specifying props.step as a dependency? In fact, we can! We can put the reducer itself inside our component to read props. [...] You may be wondering: how can this possibly work? How can the reducer “know” props when called from inside an effect that belongs to another render? The answer is that when you dispatch, React just remembers the action — but it will call your reducer during the next render. At that point the fresh props will be in scope, and you won’t be inside an effect. A Complete Guide to useEffect - Why useReducer Is the Cheat Mode of Hooks

  15. One thing that’s notable is that useReducer is strictly more powerful (which can lead to fewer optimizations). It can do something that no third-party [library] nor useState can. It can respond to changes to a parent in the same batch, has already been queued or happens at higher pri[ority]. Sebastian Markbåge tweet

  16. However, even setCount(c => c + 1) isn’t that great. It looks a bit weird and it’s very limited in what it can do. For example, if we had two state variables whose values depend on each other, or if we needed to calculate the next state based on a prop, it wouldn’t help us. Luckily, setCount(c => c + 1) has a more powerful sister pattern. Its name is useReducer. A Complete Guide to useEffect - Functional Updates and Google Docs

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