React state & memoization sandbox

Demystifying how and when React decides to (re-)render components

45 min read · Published 12 Jan 2023

React sci-fi concept logo for this app

This is an interactive sandbox for learning about how React (re-)renders components, and how you can use different forms of memoization to improve performance. For each example, you will see the background change when the component re-renders — a visual cue that helps demonstrate when and why React does (or doesn't) re-render a component. (You can also turn on paint flashing in Chrome's DevTools if you want to see how the browser is handling these changes: with DevTools open, from the ••• menu on the right, click More tools > Rendering > Paint flashing).

The basics of React rendering

The entire raison d'etre of React is to provide a developer-friendly, event-driven way to make smooth, buttery, and (most importantly) interactive changes to the DOM. To do this, React is built on top of JavaScript's event loop: it observes changes in the system and makes (usually) intelligent decisions about when to update, or repaint the DOM. React does it best to optimizes when it repaints — but often times we encounter unexpected and unwanted rendering when we are dealing with complex data types like functions and objects being passed as props (or being consumed via hooks).

React will schedule a re-render (a.k.a. repaint) of a component when its state changes, i.e. when…

  1. One of its props changes.
  2. One of its parent components is re-rendered.
  3. State from a hook it consumes changes — this includes native hooks like useState!

Felix Gerschau has a great in-depth post about how React rendering works if you want to learn more, but here's an excerpt:

The bad news is: [Even if a child component doesn't receive any props, when its parent's state changes], the render function of the child components is executed. The execution of these render functions has two drawbacks:

  1. React has to run its diffing algorithm on each of those components to check whether it should update the UI.
  2. All your code in these render functions or function components will be executed again.

The first point is arguably not that important since React manages to calculate the difference quite efficiently. The danger lies in the code that you wrote being executed repeatedly on every React render. In the example above, we have a small component tree. But imagine what happens if each node has more children, and these again might have child components?

Primitives vs. objects

or: how and when React decides to re-render

It can be straightforward to predict the behavior of your components and design around re-rendering when you are dealing with primitives types: string int float boolean null undefined. But it can get gnarly and complicated to diagnose rendering issues when you are working with objects and functions — this is because in order to React to determine if a prop has changed, it does a shallow compare: where a primitive value will always be true (e.g. "apples" === "apples"), a shallow compare of an object (remember functions are objects, too!) compares the reference (e.g. Object.is({}, {})) — meaning that if that reference changes, like if you set it or destructure it, it will never be equal even if all of its values are identical. Here's a code example of what that all means:

const apple1 = { apple: "macintosh" }
const apple2 = { apple: "macintosh" }

apple1 === apple2 // false
apple1.apple === apple2.apple // true

Now that we know this, we should tweak our list a little bit. React schedules a component to re-render when:

  1. One of its props changes — either a primitive's value, or an object's reference.
  2. One of its parent components is re-rendered.
  3. State from a hook it consumes changes.

Example 1

In which our component re-renders every time state changes

Let's get started 🚗💨. Perhaps counterintuitively, we're going to start at the end (the third bullet point above) because the easiest way to demonstrate a re-render is to trigger a state update. Thanks to hooks in React 16+, we can do this with useState.

This is a simple example of a component that re-renders every time its parent component renders. Each time you click the button, the background color will change, because the state stored in this component has changed, and any unprotected function (like ours that sets the background here) will be executed each time.

It's certainly pretty to watch the background change, but it's not very useful and can be both the cause, and a symptom of, underlying performance risks and problems.

We'll see how we can use memoization to prevent this component from re-rendering unnecessarily in the next example.

Note: these examples are using MUI components, but the concepts are applicable to any React component.

const Example1 = () => {
  const [value, setValue] = useState(0);
  const bgcolor = randomBackground();

  <Box bgcolor={bgcolor}>
    <Input value={<Input value={`Clicks: ${value}`} />} />
    <Button variant="contained" onClick={() => setValue(value + 1)}>
      Click me
    </Button>
  </Box>
};

Preventing unnecessary re-renders with useEffect

So how could we prevent component properties from changing unnecessarily? One way is to wrap our function in a useEffect hook to force the background color setter function to only runs once when the parent component is mounted. But there's a catch: we can't set a variable directly in a useEffect hook. If we try, we get:

Assignments to the bgcolor variable from inside React hook useEffect will be lost after each render. To preserve the value over time, store it in a useRef hook and keep the mutable value in the .current property. Otherwise, you can move this variable directly inside useEffect. (See: eslintreact-hooks/exhaustive-deps).

This is telling us that if we want to use useEffect to set a variable, we need to store that variable in state. This is a bit of extra work because we're now storing the background color in two places: once in state, and once in the DOM (remember, React makes hard things easy and easy things hard...) But it's a small price to pay for having control over when the component re-renders — which inevitably translates to performance gains, too.

What guarantees that this function only runs once each time the component is mounted? The second argument to useEffect is an array of dependencies. If the array is empty ([]), the function will only run once, when the component is mounted. If the array contains a value, the function will run every time that value changes. In this case, if we add value to the array, the function will run every time the value changes. Think of these dependencies as "observables" (a core concept of functional reactive programming): we can write our code to watch for specific changes so that UI updates only occur when we need them to.

OK, to be fair, that's a lot of words! What would that look like in code?

const MyComponent = () => {
  const [value, setValue] = useState(0);
  const [bgcolor, setBgColor] = useState("none");
  const [textColor, setTextColor] = useState("none");
  const [text, setText] = useState("Hello, world!");

  useEffect(() => {
    setBgColor(randomBackground());
  }, [value]);
  //  ^ by 'observing' value here, we can force 
  //    the background color to change each time value changes.

  useEffect(() => {
    setTextColor(randomTextColor());
  }, []);
  //  ^ by passing an empty array, we can force the text color to change
  //    only once, when the component is mounted or its parent re-renders.

  useEffect(() => {
    setText(`Hello from the world, value is >= ${value}!`);
  }, [value % 10 === 0]);
  //  ^ by 'observing' value with an equation that reduces to a primitive 
  //    value, we can force the text to change only when value is a 
  //    multiple of 10 — and has changed!
      
  return (
    <>
      { /* ... some JSX here */ }
    </>
  );
};

Example 2

Memoizing via useEffect with an empty dependency array

Here's our random background example altered to use useEffect instead. Now when you click the button, the background no longer changes! We can still force the background color to change — either by adding value to the array of dependencies (which would cause it to update when our primary button is clicked), or by setting the background color directly in another button's onClick handler.

const Example2 = () => {
  const [value, setValue] = useState(0);
  const [bgcolor, setBgColor] = useState("none");

  useEffect(() => {
    setBgColor(randomBackground());
  }, []);

  <Box bgcolor={bgcolor}>
    <Input value={<Input value={`Clicks: ${value}`} />} />
    <Button variant="contained" onClick={() => setValue(value + 1)}>
      Click me
    </Button>
    <Button
      variant="outlined"
      startIcon={"🔨"}
      onClick={() => {
        setValue(value + 1);
        setBgColor(randomBackground());
      }}
    >
      Force background change
    </Button>
  </Box>
};

Example 3

Memoizing with good old-fashioned useMemo

Another way we can solve our trigger-happy re-rendering problem is by memoizing the background color property instead of storing it in a separate state with useMemo. This is a good solution for expensive computed properties (as long as they're read-only) — because unlike with useEffect, which requires both state and the effect function to set it, we can just wrap our property in a single useMemo. Not only is that less code to write, we also end up with just a single property to consume.

And, just the same as we did with useEffect, we can pass an array of dependencies to useMemo to tell it when to update (though for our purposes we explicitly don't want that).

const Example3 = () => {
  const [value, setValue] = useState(0);

  // before:
  // const [bgcolor, setBgColor] = useState("none");
  // 
  // useEffect(() => {
  //   setBgColor(randomBackground());
  // }, []);

  // after:
  const bgcolor = useMemo(() => randomBackground(), []);

  // if we wanted to update the background color when value changes:
  // const bgcolor = useMemo(() => randomBackground(), [value]);
  //     ...we can pass the value in useMemo's deps array ^

  return (
    <Box bgcolor={bgcolor}>
      <Input value={<Input value={`Clicks: ${value}`} />} readOnly />
      <Button variant="contained" onClick={() => setValue(value + 1)}>
        Click me
      </Button>
    </Box>
  );
};

From our hitlist, we've now seen components re-render because:

  1. One of its props changes — either a primitive's value, or an object's reference.
  2. One of its parent components is re-rendered.
  3. ✅ State from a hook it consumes changes.

And we've covered the basics of what to do about it: when we updated the state without any guards around the background color in the first example, the background color changed every time we clicked the button, but when we memoized it using useEffect or useMemo, we reclaimed some control over its state.

Now, let's look at #2: a parent component with state that causes its child components re-render.

Example 4

Passing props to child components

To demonstrate this, we'll use the useMemo hook to memoize our background color in the parent component so that we don't get any false-positive re-rendering, then we'll wrap our example component inside of a second random-background component to act as its parent.

Child component 1

This component will re-render every time its parent component re-renders, because even though we've memoized the background color in the parent, the child will be re-drawn each time the parent's state changes. And when that happens, each of the child's un-guarded functions is executed each time the parent's value prop changes.

Child component 2

Thankfully, we already have the tools we need to fix this! We just need to memoize the child's background color, too. And as always, if we don't pass useMemo or useEffect any dependencies, the background color will only re-render when the component is actually re-mounted (i.e., when the element is removed from the DOM).

To test this, we can create a new state value for a second child, then wrap our child component's background color in useMemo. You'll notice something is off: no matter which button you click, the first component will always re-render! This is because we only memoized the background color for our second child — but in both cases, the parent's state has changed.

const Example4 = () => {
  const [value1, setValue1] = useState(0);
  const [value2, setValue2] = useState(0);

  const memoBgColor = useMemo(() => randomBackground(), []);

  return (
    <>
      {/* Child component 1 */}
      <Box id="child-component-1" bgcolor={randomBackground()} p={2} mb={2}>
        { /* This one will always update */ }
      </Box>

      {/* Buttons and inputs */}
      <Button
        variant="contained"
        onClick={() => setValue1(value1 + 1)}
        startIcon={<ChevronUpIcon />}
      >
        Update 1
      </Button>
      <Input value={`Clicks: ${value1}`} readOnly />

      <Button
        variant="contained"
        onClick={() => setValue2(value2 + 1)}
        endIcon={<ChevronDownIcon />}
      >
        Update 2
      </Button>
      <Input value={`Clicks: ${value2}`} readOnly />

      {/* Child component 2 */}
      <Box id="child-component-2" bgcolor={memoBgColor} p={2} mb={2}>
        { /* This one won't, because bgcolor is memoized */ }
      </Box>
    </>
  );
};

This is a good demonstration of how memoization is critical when working with complex nested components: it's tough to predict how a parent's state will change, and even if your component doesn't depend on the parent's state, it will still be re-rendered.

A good rule of thumb as the component creator is to consider how your component will be used, and to make sure that it has some reasonable guardrails to make sure it doesn't re-render unnecessarily.

OK, let's see where we're at:

  1. One of its props changes — either a primitive's value, or an object's reference.
  2. ✅ One of its parent components is re-rendered.
  3. ✅ State from a hook it consumes changes.

On to the next example!

Example 5

Passing different types of props

Now we're going to look at what happens when we pass a few different types of props. To demonstrate this, we'll store these in state:

  • A function
  • A Date object
  • A data object, like {fruit: ["apples","pears","peaches"], oogway: "wise"}}
  • A simple boring string

To simulate our state changing, we have to force the object references to update. Imagine for each button click, our state is being refreshed from a server. This doesn't always mean the values have been changed, but we'll still be able to see the effect of the component re-rendering because the data object is being replaced with a new one.

Give it a try:

What do we notice?

  1. The function stored in state doesn't trigger an update because React is clever — if the returned value is a primitive and hasn't changed, it won't re-render the component. In the button example below, we set the function state again with a different returned value, but the background only changes once (because it's always the same after that). What would happen if we returned an object?

  2. The date is an object (an instance of a Date class), so it always triggers an update even if the actual date value hasn't changed.

  3. The object is... well, an object, so it will always trigger an update.

  4. Lastly, as it always has, the boring string value never changes, so the child never re-renders... unless we pass it as a prop to a child! Then it will re-render if the value of the string (primitive) prop changes. For this example, you'll see the background change each time the string value changes (but not when it stays the same):

const Example5 = () => {
  const [value, setValue] = useState(0);
  const backgroundColor = randomBackground();

  return (
    <>
      <Input value={<Input value={`Clicks: ${value}`} />} />
      <Button variant="contained" onClick={() => setValue(value + 1)}>
        Click me
      </Button>
    </>
  );
};

The plot thickens! That's a lot of potential re-renders! Especially if we're working with something like SWR which polls and refreshes our data. (Caveat: SWR has some good tooling for deeply comparing data to decide whether it should update state, but that's another story).

In short: for large, complex React apps, it's entirely impractical to manage this without some careful memoization, because your entire team isn't magic 🧙‍♀️ and omiscient 🔮! And this problem becomes exponentially dangerous if we're passing these values to a tree of child components that are re-rendered unnecessarily. Imagine if in your tree of components, some are responsible for fetching their own data? That means that for each re-render, they'll be re-fetching data, too... 💀

Demo time 🕵️‍♂️

Example 6

Simulating API requests and exploring how changed objects causes re-renders

In this example, each time you click the button, we'll simulate an API request. You'll see the component re-render each time we click a button, even when the data doesn't actually update.

    Click a button to load some data...

What's very important to note here is the difference in how we update the posts state.

  1. The first button changes the data, so we actually would expect it to update.
  2. The second button uses the spread ... operator to destructure posts: setPosts([...posts]); even though the data is the same, destructuring creates a new object, thus the reference changes and it re-renders.
  3. The third button updates the state but both the data and the reference haven't changed, so it never updates: setPosts(posts).

What if we make this more complicated? Let's try it with an object where we change the value of a nested property. Will it re-render? Let's find out:

{ "a": { "b": { "c": 1 } } }
const Example6 = () => {
  const [posts, setPosts] = useState<{ id: number; title: string }[]>([]);
  const [nestedObject, setNestedObject] = useState({
    a: { b: { c: 0 } },
  });

  const [bgcolor] = useRandomBackground([posts, nestedObject]);
  //                ^ notice the new shiny custom hook for our random background, too?
  //                  This is actually essential for this app to work, because it runs
  //                  on Next.js (which gets mad about server-side styling).

  return (
    <>
      <Input value={<Input value={`Clicks: ${value}`} />} />
      <Button variant="contained" onClick={() => setValue(value + 1)}>
        Click me
      </Button>
    </>
  );
};

Real-world example: a search component

It's probably overdue at this point that we stitch everything we've covered into an actual real-world example — not only because it'll help deliver the zinger punchline of why we want to memoize most of our React things, but it's helpful to see how this all comes together in an actual app.

(Admittedly this is a digression from our main goal of demystifying memoization, but a necessary one — because we need to cover all the ways things can go wrong before we talk about ways to fix them).

In our first attempt, we'll stuff all of our logic into a single component, then we'll refactor it into a custom hook to make our component more readable. After, we'll see if we can spot any re-rendering risks.

Search component: attempt #1

import { useState } from "react";
import { useDebounce } from "hooks/useDebounce";
import { Box, Input, List, ListItem } from "@mui/material";

const SearchResults = () => {
  const [searchTerm, setSearchTerm] = useState("");
  const [searchResults, setSearchResults] = useState([]);
  const [isLoading, setIsLoading] = useState(false);

  const debouncedSearchTerm = useDebounce(searchTerm, 500);

  useEffect(() => {
    if (debouncedSearchTerm) {
      setIsLoading(true);
      fetch("/api/search?query=" + debouncedSearchTerm)
        .then((res) => res.json())
        .then((results) => {
          setSearchResults(results);
          setIsLoading(false);
        });
    } else {
      setSearchResults([]);
    }
  }, [debouncedSearchTerm]);

  return (
    <Box>
      <Input
        type="text"
        placeholder="Search..."
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
      />
      {isLoading ? (
        <Box>Loading...</Box>
      ) : (
        <List>
          {searchResults.map((result) => (
            <ListItem key={result.id}>{result.title}</ListItem>
          ))}
        </List>
      )}
    </Box>
  );
};

That's a good start, but consider that this is a very simple example. Depending on what else is going on in our component, we might have another dozen useEffect events happening in this one component — making it hard to understand what each is doing, and why. Let's see if we can break it down into a custom, declarative hook:

Refactored into a custom hook: attempt #2

We know that React hooks are a powerful addition to the React arsenal. The built-in hooks like useState, useMemo, useEffect, and useCallback are the essential building blocks for managing state, but like any procedural code, our cognitive ability to understand what's going on in a complex component with dozens of state variables and useEffect hooks to manage them can decline quite quickly.

Thankfully, we can easily create our own custom hooks — which allow us to build declarative, more semantic components by abstracting hard-to-read/understand logic into sensible chunks. Still, as with any state consumed by a component, they can also lead to unexpected (and complicated-to-diagnose) re-renders. Let's consider in our next example that we're storing some search results.

import { useState } from "react";
import { useDebounce } from "hooks/useDebounce";
import { Box, Input, List, ListItem } from "@mui/material";

const useSearch = (initialSearchTerm) => {
  //  ^ Remember, hooks *must* start with "use"!
  const [searchTerm, setSearchTerm] = useState(initialSearchTerm);
  const [searchResults, setSearchResults] = useState([]);
  const [isLoading, setIsLoading] = useState(false);

  const debouncedSearchTerm = useDebounce(searchTerm, 500);

  useEffect(() => {
    if (debouncedSearchTerm) {
      setIsLoading(true);
      fetch("/api/search?query=" + debouncedSearchTerm)
        .then((res) => res.json())
        .then((results) => {
          setSearchResults(results);
          setIsLoading(false);
        });
    } else {
      setSearchResults([]);
    }
  }, [debouncedSearchTerm]);

  return {
    searchTerm,
    setSearchTerm,
    searchResults,
    isLoading,
  };
};

const SearchResults = () => {
  const {
    searchTerm,
    setSearchTerm, 
    searchResults,
    isLoading,
  } = useSearch("");
  //  ^ We can now use our custom hook in our component! This is 
  //    much more readable, and we can see at a glance what its 
  //    purpose is and how to use it, without having to dig through
  //    the implementation. Plus, we can reuse it in other 
  //    components. 💃

  return (
    <Box>
      <Input
        type="text"
        placeholder="Search..."
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
      />
      {isLoading ? (
        <Box>Loading...</Box>
      ) : (
        <List>
          {searchResults.map((result) => (
            <ListItem key={result.id}>{result.title}</ListItem>
          ))}
        </List>
      )}
    </Box>
  );
};

That's much better — but...

In both of these examples, we're making an API call to fetch our search results, then updating the state with the results. As we now know, even if the search results are the same, React will still re-render our component because the object reference stored in state has changed. This is of course what we would want if the user is actively typing into a search box, but what if we were using SWR to poll for periodic updates to a list of, say, social media posts — where the system is polling for updates once every second? That means that every child in the tree within the parent component will be re-rendered, even if there are no data changes, once per second.

And from a reusability standpoint, there's another catch. We know that when we create state in a React component, it is scoped to that component and its children. This means that if we want to use the same shared state in our child components, we can't just go re-using our custom hook in each child — we'll end up with three different copies of our hook state in each child. What happens if we try to re-use our hook in two components?

Accidentally creating multiple instances of state

const SearchResults = () => {
  const {
    searchTerm,
    setSearchTerm, 
    searchResults,
    isLoading,
  } = useSearch("");

  return (
    <Box>
      <Input
        type="text"
        placeholder="Search..."
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
      />
      {isLoading ? (
        <Box>Loading...</Box>
      ) : (
        <SearchResultsList />
      )}
    </Box>
  );
};

const SearchResultsList = () => {
  const {
    searchTerm,
    setSearchTerm,
    searchResults,
    isLoading,
  } = useSearch("");
  //  ^ This is a separate instance of the hook state, so it won't be
  //    in sync with the state in the parent component. 😢 Worse, 
  //    because we're making an API call in the hook, we'll end up
  //    making a lot of extra requests to get the same data. (And
  //    we're not even going to talk about what the search term 
  //    being stored in two different places breaks!)

  return (
    <List>
      {/* ... draw the rest of the owl */}
    </List>
  );
};

And there's the rub: we've been talking about memoizing state to prevent unnecessary re-renders — if we're not careful, we can cause an O(n^m) (that's big-O notation for very exponentially bad) cascade of re-rendering components. So what can we do? Aside from craftfully memoizing (or a complete re-engineering with WebSockets), we also need to make sure that our state and hooks aren't being duplicated:

Drilling props like a boss

We could prop-drill our state from the parent down into the children:

const SearchResults = () => {
  const {
    searchTerm,
    setSearchTerm,
    searchResults,
    isLoading,
  } = useSearch("");

  return (
    <Box>
      <Input
        type="text"
        placeholder="Search..."
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
      />
      {isLoading ? (
        <Box>Loading...</Box>
      ) : (
        <SearchResultsList
          searchTerm={searchTerm}
          setSearchTerm={setSearchTerm}
          searchResults={searchResults}
          isLoading={isLoading}
        />
        {/* ^ This is a lot of props to pass through, 
              and that's just one component! 😵‍💫 */}
      )}
    </Box>
  );
};

...but it quickly becomes evident that the more state we have to pass down, the messier and more unmanageable this gets (and we'll still be victims of the parent's state triggering re-renders, even if we prop-drill).

We need to be able to share state across components, but we don't want to have to pass it down through props. 🤔 🌮

Sharing state with React context

The cleanest way to do this (natively, of course, we're not going to go down the Redux rabbit hole) is with React context. Creating a context provider will allow our child components to consume our state in a way that's much more readable and maintainable than prop-drilling. We can also expose our context provider through a custom hook, so that we can use it in our components just like we did in the previous example — but this time, with the benefit of shared state. And lastly, (and arguably the most important of all), we can have a central place to manage how we memoize our state.

Search component with a React context provider: attempt #3

// Path: components/search/Search.provider.tsx
const SearchContext = React.createContext<SearchContextType | null>(null);

const SearchProvider = ({ children }: PropsWithChildren<{}>) => {
  const {
    searchTerm,
    setSearchTerm,
    searchResults,
    isLoading,
  } = useSearch("");
  //  ^ neat, we can use our custom hook here in our
  //    provider for even more declarative goodness!

  return (
    <SearchContext.Provider
      value={{
        searchTerm,
        setSearchTerm,
        searchResults,
        isLoading,
      }}
    >
      {children}
    </SearchContext.Provider>
  );
};

const useSearchContext = () => {
  const context = React.useContext(SearchContext);
  if (!context) {
    throw new Error("useSearchContext must be used within a SearchProvider");
  }
  return context;
};
// Path: components/search/Search.tsx
const SearchInput = () => {
  const { searchTerm, setSearchTerm } = useSearchContext();

  return (
    <Input
      type="text"
      placeholder="Search..."
      value={searchTerm}
      onChange={(e) => setSearchTerm(e.target.value)}
    />
  );
};

const SearchResults = () => {
  const { searchTerm, searchResults, isLoading } = useSearchContext();

  return (
    <List>
      {/* ... draw the rest of the owl */}
    </List>
  );
};

export const Search = () => {
  return (
    <SearchProvider>
      <Box>
        <SearchInput />
        <SearchResults />
      </Box>
    </SearchProvider>
  );
};

Great! We were able to slice this up into more manageable chunks of code (and child components), and now all our state is managed in one central place, so we don't have to worry about multiple instances of it.

But: abstracting logic into a custom hook and/or context provider doesn't magically prevent re-rendering, and in fact one (potential) drawback of these abstractions is that it can make it a little harder to notice and diagnose code that might cause a re-render:

  • We could end up making some future decisions without remembering to look at exactly what the hook is doing.
  • Providers unlock the ability to create many small components powered by the same shared state, which can make it hard to keep track of the inevitable tree of nested children (and which ones might be causing re-renders).

...which is fine, now that we know to keep an eye out for it, right?

Demystifying re-renders when working with complex objects

Finally, now that we have laid out a small planetoid's worth of groundwork and backstory and digressions, we are ready to figure out where our re-rendering risks are, and what we can do about it. As luck would have it, we actually have introduced at least one re-rendering risk in each and every one of these examples 🤯.

As we covered in the last example, we now know that objects are not memoized by default.

(What's that? If you missed the memo, you can read the Wikipedia entry for memoization, or the tl;dr: memoizing means to store the computed value of something expensive so we don't have to keep recomputing it.)

If we pass an object as a prop to a child component, and that object's reference changes, the child component will re-render. The same is true whether we use a custom hook, a context provider, or a context provider with a custom hook, because each time we update that object, even if its values don't change, the object will be a new reference.

» Memoization has entered the chat

Example 7

What happens when our complex objects aren't memoized?

In examples 2 & 3 we covered useEffect and useMemo as the two easiest ways to memoize state (and props) in React. But they won't work for complex objects, because they use the same underlying reference comparison to determine if the value has changed. Let's see what happens when we try to use them with a complex object:

{ fruit: "pineapple" }

In each of our previous examples where passing a state value as a prop (or exposing it via context provider, and/or a custom hook) caused children to re-render, memoizing those primitive types worked. But since basket is an object, it turns out that even when memoized at source (consuming memoBasket instead) doesn't make a difference.

const Example7 = () => {
  const [basket, setBasket] = useState({ fruit: "pineapple" });
  const memoBasket = useMemo(() => basket, [basket]);
  
  const [bgcolor] = useRandomBackground([memoBasket]);

  <>
    <Button
      variant="contained"
      onClick={() => {
        setBasket({ fruit: "mango" });
      }}
    >
      Set to mango
    </Button>
    <Button variant="outlined" onClick={() => setBasket({ fruit: "pineapple" })}>
      Set to pineapple
    </Button>
    <Code>{`{ fruit: "${basket?.fruit}" }`}</Code>
  </>
};

Memoizing complex objects

There are two ways we can work around this:

  1. "Observe" only a primitive portion of the object and silence the ESLint warning.
  2. Deeply compare the properties of the object inside of a useEffect or useMemo.

Depending on which option we want to use, there are a few tools we have at our disposal:

  1. Use packages like Kent C. Dodds' O.G. useDeepCompareEffect, or the more comprehensive Use Deep Compare (our personal favorite). These packaged hooks behave like the built-in React hooks, but instead deeply compare all deps.
  2. usePrevious stores the previous version of state which can be used when comparing.
  3. react-fast-compare (a React-focused fork of fast-deep-equal) can deeply compare object properties.
  4. And of course we can write our own comparison function inside of useEffect or useMemo.

Really important to call out is this note from useDeepCompareEffect:

NOTE: Be careful when your dependency is an object which contains a function. If that function is defined on the object during a render, then it's changed and the effect callback will be called on every render. This issue has more context.

So, if you're using useDeepCompareEffect and you're passing a function as a prop or object property, you'll need to wrap it in a useCallback hook (or useDeepCompareCallback) to prevent it from being redefined on every render. We'll cover more on useCallback shortly.

A little on what not to do

  1. Don't use JSON.stringify to deeply compare objects. It's slow and buggy, because not only is it possible to have functions or classes that don't stringify into JSON, it only works if the object's keys and values are in exactly the same order. (Objects are unordered in JavaScript, so this is bad.)

  2. You might be tempted to be clever and just update properties before you set your state, like this:

const [state, setState] = useState({ maine: "new-england-chowder" });
state.maine = "manhatten-chowder";
setState(state);

Oh, no, ohh no, this is an abhorrent thing to do. Not only is the chowder an abomination, but trying to update state like this is a recipe for disaster. You'll end up with the opposite problem: React will not re-render your component when you do this, because the object's reference never changed, so it will never know that you've changed the state.

It's also extra not a good idea because React's state handling is asychronous, meaning there's a non-zero chance your new value will be lost forever. Even if you tried to use the callback form of setState to avoid that, you'd still have the same problem.

const [state, setState] = useState({ maine: "new-england-chowder" });
state.maine = "manhatten-chowder";
setState((prevState) => {
  prevState.maine = "manhatten-chowder";
  return prevState;
});

Thankfully in this case, we didn't want to ruin a perfectly good chowder, so no harm done.

Example 8

Preventing re-renders by comparing specific object properties

This technique perhaps isn't authentic memoization per se, but it does let us hack useMemo a bit to handle objects. Let's say we have determined that the only property that matters if it changes is the fruit property of our basket object. We can adjust the useEffect or useMemo deps to compare just the fruit property instead of the whole object. Keep in mind that this will only work if the fruit property is a primitive value (string, number, boolean, etc.).

Let's try to use useMemo to memoize our basket object. We'll use the same equality function as in the previous example, but this time we'll observe just the value of basket?.fruit. This is quite effective, but consider using with care: it requires you to know all the properties of the object you need to observe in advance. If you're only checking some of them, you might miss some updates.

{ fruit: "pineapple" }

This child generates its own random background; it will re-render each time the parent's state does, but you'll see that the parent only changes the value of basket?.fruit does. Hrm... 🤔

// Code for this example

const Example8 = () => {
  const memoBasketCustomCompare = useMemo(() => basket,
    // To prevent the following warning, we can add an eslint-disable comment:
    //     React Hook useMemo has a missing dependency: 'basket'. Either include it or remove the dependency array.
    //     eslintreact-hooks/exhaustive-deps

    // eslint-disable-next-line react-hooks/exhaustive-deps
    [basket?.fruit]
    //        ^ now we're observing the fruit property of the basket object
  );

  const [bgcolor] = useRandomBackground([memoBasketCustomCompare]);

  return (
    <ExampleSection bgcolor={bgcolor} id="parent">
      <Button
        variant="contained"
        startIcon={"🥭"}
        onClick={() => setBasket({ fruit: "mango" })}
      >
        Set to mango
      </Button>
      <Button
        startIcon={"🍍"}
        variant="outlined"
        onClick={() => setBasket({ fruit: "pineapple" })}
      >
        Set to pineapple
      </Button>
      <ExampleSection id="child" />
    </ExampleSection>
  );
}

Memoizing objects with useDeepCompareMemo

useDeepCompareMemo from https://github.com/sandiiarov/use-deep-compare is a drop-in replacement for useMemo, and the package comes with drop-in replacements for useEffect and useCallback, too. That just makes life easy — we can spend less time worrying about whether something isn't properly being memoized, and get back to writing code.

Example 9

Memoizing objects with useDeepCompareMemo

useDeepCompareMemo will deeply compare the old and new value (i.e., check each key/value pair of the object, recursively, to determine if any values have changed). This is a great option if you need to compare all of the object's properties — and though it can have a (small) performance cost, it uses dequal under the hood, so it's still very fast.

{ fruit: "pineapple" }
import { useDeepCompareMemo } from "use-deep-compare";

const Example9 = () => {

  // Option 2: Use the useDeepCompareMemo hook from use-deep-compare
  const deepMemoBasket = useDeepCompareMemo(() => basket, [basket]);
  //                            pass the whole object safely ^

  const [bgcolor] = useRandomBackground([deepMemoBasket]);

  return (
    <ExampleSection bgcolor={bgcolor}>
      <Button
        variant="contained"
        startIcon={"🥭"}
        onClick={() => setBasket({ fruit: "mango" })}
      >
        Set to mango
      </Button>
      <Button
        startIcon={"🍍"}
        variant="outlined"
        onClick={() => setBasket({ fruit: "pineapple" })}
      >
        Set to pineapple
      </Button>
    </ExampleSection>
  );
}

Memoizing objects with usePrevious and react-fast-compare

Now let's try a combination of usePrevious from https://usehooks.com/usePrevious and react-fast-compare from https://github.com/FormidableLabs/react-fast-compare. This requires us to write a little more code, but it gives us greater flexibility in how we compare the old and new values. We can also use this same approach when using SWR, which already maintains its own state.

There are other deep comparison libraries out there — like lodash's _.isEqual, but none benchmark as fast as react-fast-compare (which is based on fast-deep-equal).

Example 10

Memoizing objects with usePrevious and react-fast-compare

usePrevious and react-fast-compare will require a slightly different approach, because usePrevious requires an existing state value in order to update itself. This results in more code, but sometimes you need to separate the parts out like this depending on how your component is structured.

{ fruit: "pineapple" }
import isEqual from "react-fast-compare";
import { usePrevious } from "lib/usePrevious";

const Example10 = () => {

  // Option 3: usePrevious with react-fast-compare
  const [basket, _setBasket] = useState({ fruit: "pineapple" });
  const prevBasket = usePrevious(basket);
  const setBasket = (newBasket: FruitBasket) => {
    if (!isEqual(prevBasket, newBasket)) {
      _setBasket(newBasket);
    }
  };

  const [bgcolor] = useRandomBackground([basket]);

  return (
    <ExampleSection bgcolor={bgcolor}>
      <Button
        variant="contained"
        startIcon={"🥭"}
        onClick={() => setBasket({ fruit: "mango" })}
      >
        Set to mango
      </Button>
      <Button
        startIcon={"🍍"}
        variant="outlined"
        onClick={() => setBasket({ fruit: "pineapple" })}
      >
        Set to pineapple
      </Button>
    </ExampleSection>
  );
}

Custom state setter with useReducer

We can also use useReducer to create a custom setter with our deep comparison baked in, instead of exposing a secondary state prop via usePrevious. (Fun fact! useReducer is actually what useState and useMemo are built on.)

This is perhaps a little less readable, but is arguably the most flexible and customizable way to manage our state:

// Option 4: useReducer with custom state setter implementation

import isEqual from "react-fast-compare";

// ... 🦉

const [basket, setBasket] = useReducer(
  (state: FruitBasket, newState: FruitBasket) => {
    if (isEqual(state, newState)) {
      return state;
    }
    return newState;
  },
  { fruit: "pineapple" }
);

const [bgcolor] = useRandomBackground([basket]);

// ... 🦉

Memoizing inbound state from SWR

And here's how it might look with SWR. Sometimes we might want to maintain our own state (if there are transformations or conditions that happen outside the fetch response), or we can use SWR's built-in compare callback (it uses https://github.com/shuding/stable-hash by default, but we can use our own if we want):

// Option 1: Our own state with react-fast-compare

import isEqual from "react-fast-compare";
import useSWR from "swr";

const BasketOfFruit = () => {

  const [basket, setBasket] = useReducer(
    (state: FruitBasket, newState: FruitBasket) => {
      if (isEqual(state, newState)) {
        return state;
      }
      return newState;
    },
    { fruit: "pineapple" }
  );

  const { data } = useSWR("/api/fruit", fetcher);
  //                                      ^ what is this? see the "fetcher" section below

  useEffect(() => {
    if (data) { // <--  we only want to update if we have data
      setBasket(data); // <-- our setter is smart enough to only update if the new state is different!
    }
  }, [data]);

  return (<>🦉</>);
};
// Option 2: SWR's built-in compare callback

import isEqual from "react-fast-compare";
import useSWR from "swr";

const BasketOfFruit = () => {

  const { data: basket } = useSWR("/api/fruit", fetcher, {
    compare: isEqual,
  });

  return (<>🦉</>);
};

SWR fetcher

The fetcher function is a simple async function that returns the data we want to use. You can check out the SWR docs for how to use it, but here's their very simple example:

const fetcher = (...args) => fetch(...args).then(res => res.json())

Memoizing React components

You might remember from Example 8, we discovered that our child component was re-rendering, even though it wasn't consuming any props or state from the parent. Sometimes our child components do things like fetch data, or perform calculations, and we don't want to re-run those expensive computations. In a pristine world, maybe we'd be in there refactoring those components not to do that, but software is messy, and perfect is the enemy of good — so let's be pragmatic instead.

There are two ways we can deal with this at the parent level:

  1. We can use React.memo to memoize the component, which means it will only re-render if the props have changed (but spoiler, that won't work with object props!)
  2. Or we can use useDeepCompareMemo to memoize the component instance. This is a great way to optimize performance, but it can also be a source of bugs if we're not careful.

Let's take a look.

Example 11

Memoizing React components with React.memo

Let's revisit Example 8, where we were using useMemo and listening for basket?.fruit to change. This time, let's wrap our child component in React.memo, which lets the component skip re-rendering when its props are unchanged. If we're only passing primitive props in, there's no need to do anything else, but if we end up passing objects or functions in as props, this function takes an optional param for determining if the props have changed: memo(Component, arePropsEqual?). If you do this, make sure you read up on the reference for arePropsEqual?, because there are some pitfalls.

{ fruit: "pineapple" }

This child generates its own random background; it will re-render each time the parent's state does, but you'll see that the parent only changes the value of basket?.fruit does. Hrm... 🤔

// Wrap our child component in React.memo()
const MemoExampleSection = React.memo(ExampleSection);
          
const Example11 = () => {
  const memoBasketCustomCompare = useMemo(() => basket,
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [basket?.fruit]
  );

  const [bgcolor] = useRandomBackground([memoBasketCustomCompare]);

  // For debugging purposes, let's add a couple of 
  // useEffects that will log when our state changes.
  useEffect(() => {
    console.log("basket changed");
  }, [memoBasketCustomCompare]);

  // It's important to make separate useEffects for each
  // state variable you want to listen for, otherwise
  // you won't know which one changed.
  useEffect(() => {
    console.log("bgcolor changed");
  }, [bgcolor]);

  return (
    <ExampleSection bgcolor={bgcolor} id="parent">
      <Button
        variant="contained"
        startIcon={"🥭"}
        onClick={() => setBasket({ fruit: "mango" })}
      >
        Set to mango
      </Button>
      <Button
        startIcon={"🍍"}
        variant="outlined"
        onClick={() => setBasket({ fruit: "pineapple" })}
      >
        Set to pineapple
      </Button>
      <MemoExampleSection id="child" />
      {/* ^ using the new memoized component */}
    </ExampleSection>
  );
}

Wait, what?! We wrapped our child in React.memo, but it's still re-rendering? Turns out, that's because the child component is setting its own background dynamically. React.memo will prevent a component from re-rendering when its props change, but it won't prevent a component's internal state from causing it to re-render.

😭 deep compare sadness.

Alright, what can we do? We can use useMemo or useDeepCompareMemo to memoize the component instance instead. React's docs even tell us that's what we should do:

When you use memo, your component re-renders whenever any prop is not shallowly equal to what it was previously. This means that React compares every prop in your component with the previous value of that prop using the Object.is comparison. Note that Object.is(3, 3) is true, but Object.is({}, {}) is false. To get the most out of memo, minimize the times that the props change. For example, if the prop is an object, prevent the parent component from re-creating that object every time by using useMemo.

Example 12

Memoizing React component's instances with useMemo

Here we are again, with our trusty basket?.fruit. Where we previously tried using React.memo to memoize our component definition (think of component definition as synonymous with class), this time, we're going to create a memoized instance of the component instead with useMemo (or our drop-in replacement, useDeepCompareMemo). This way the component only gets mounted once, and the parent can control when it is re-rendered. It's important to note that once you memoize a component instance, you can no longer pass it props, because it has already been rendered.

If you're using TypeScript: you can memoize many things with useMemo and its return type will remain the same, but once you memoize a React component, instead of returning a component definition (i.e. React.ReactNode), it will return a component instance — JSX.Element.

The code to do this looks a bit like 🍝, so here's a quick breakdown:

// Simple memoization of variables looks like this:
const memoizedValue = useMemo(
  //                     ^ call useMemo
  // then pass in a function as its first param
  // that returns the value you want to memoize:
  () => someExpensiveComputation(),

  // then pass in an array of dependencies:
  [stateToWatch, propToWatch]
);

// In our case, we want to memoize a component instance, 
// so we'll pass in a function that returns a JSX element:
const memoizedInstanceOfMyComponent = useMemo(
  () => <MyComponent {...props} />,
  [props]
);

// Now, we can use our memoized component instance as a variable inside other JSX:
return (
  <div>
    {memoizedInstanceOfMyComponent}
  </div>
);

// (Notice how this different than directly returning the
// component as JSX — we're not returning this anymore):
// return (
//   <div>
//     <MyComponent {...props} />
//   </div>
// );

// And of course useDeepCompareMemo works, too:
const { data } = useSWR('/api/expensive-endpoint', fetcher);

const deepMemoizedInstance = useDeepCompareMemo(
  () => <MyComponent {...data} />,
  [data]
);

Time to see our memoized component instance in action 🚀! We've solidly protected ourselves against our (admittedly rather poorly-designed) child component re-rendering when we don't want it to.

{ fruit: "pineapple" }

This child generates its own random background; but now that we've wrapped it in useMemo, it will no longer re-render each time the parent's state does! 🎉

const Example12 = () => {
  
  const [basket, setBasket] = useState({ fruit: "pineapple" });

  const memoBasketCustomCompare = useMemo(
    () => basket,
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [basket?.fruit]
  );

  const [bgcolor] = useRandomBackground([memoBasketCustomCompare]);

  const exampleChild = useMemo(
    () => (
      <ExampleSection parentBgColor={bgcolor}>
        <Markdown>
          {
            "This child generates its own random background; but now that ... 🎉"
          }
        </Markdown>
      </ExampleSection>
    ),
    [bgcolor]
  );

  return (
    <ExampleSection bgcolor={bgcolor} id="parent">
      <Button
        variant="contained"
        startIcon={"🥭"}
        onClick={() => setBasket({ fruit: "mango" })}
      >
        Set to mango
      </Button>
      <Button
        startIcon={"🍍"}
        variant="outlined"
        onClick={() => setBasket({ fruit: "pineapple" })}
      >
        Set to pineapple
      </Button>
      {exampleChild}
      {/* ^ using the variable we created instead of JSX */}
    </ExampleSection>
  );
}

Well, folks, that's really the nuts and bolts of memoization.

  1. ✅ One of its props changes — either a primitive's value, or an object's reference.
  2. ✅ One of its parent components is re-rendered.
  3. ✅ State from a hook it consumes changes.

There's just one more thing to cover that we should talk about and haven't yet: memoizing functions and useCallback...

Functions are objects, too

As we've alluded to a couple of times, functions in JavaScript are objects, too. They can be passed around as props or stored as state, and like objects, they are compared by reference, not by value — so there's a good chance we'll end up with a bunch of unnecessary re-renders each time a function is re-created. Unlike regular data objects however, in order to be executable, functions need to be memoized with useCallback (which returns a memoized function) instead of useMemo (which returns a memoized value).

Luckily it works the same way as useMemo, so there's not much to it:

Example 13

Memoizing functions with useCallback

In this example, we're going to make custom hook with memoized state, and memoized state handlers. We're going to make... you guessed it, a random background generator hook 🤭.

Why? Because this project is built on Next.js, which has baked in server-side rendering of pages, it complains at us when we load pages with a state mismatch. In all of the previous examples, we've actually been using this custom hook all along, becuase Next.js doesn't like it when we use our raw randomBackground() function in a React component:

// Oh no, Next.js will be mad at us!
// Drake meme: "I'm upset, talk to the hand 👋🏾"        
const BadComponent = () => {
  const bgcolor = randomBackground();

  return (
    <FlexBox bgcolor={bgcolor}>
      {/* ... */}
    </FlexBox>
  );
}
Warning: Prop `className` did not match. 
Server: "(some server style)" Client: "(some client style)"
// Drake meme: "Much better, much better 😄"
const GoodComponent = () => {
  const [bgcolor] = useRandomBackground();

  return (
    <FlexBox bgcolor={bgcolor}>
      {/* ... */}
    </FlexBox>
  );
}

Let's build it!

We are going to make a custom hook to wrap our randomBackground() function in, then wrap its handler functions in useCallback to prevent them from being recreated on every render. This gives us a nice declarative hook to use our background, and we memoize as close to the source as possible so that whoever is consuming it doesn't have to worry about the implementation.

{ fruit: "pineapple" }
// Path: lib/useRandomBackground.ts
import { useCallback, useMemo } from "react";
import { randomBackground } from "./random";
export const useRandomBackground = (deps: DependencyList = []) => {
  const [bgcolor, setBgColor] = useState<FlexBoxProps["bgcolor"]>("none");
  const refresh = useCallback(() => {
    setBgColor(randomBackground());
    //           ^ notice our random background function is *executed* in here, 
    //             which is why want to memoize the refresh function. If we didn't,
    //             we'd be creating a new random background on every render.
  }, [...deps]);
  //  ^ destructuring deps array allows the consumer to control when and how it is memoized

  return [bgcolor, refresh] as const;
};

// Path: components/RandomBackgroundExample.tsx
export const RandomBackgroundExample = (props: ExampleProps) => {
  const [basket, setBasket] = useState({ fruit: "pineapple" });
  const deepMemoBasket = useDeepCompareMemo(() => basket, [basket]);

  const [bgcolor, refresh] = useRandomBackground([deepMemoBasket]);
  // notice we're passing in our memoized basket here   ^
  // so that when the value of basket changes, the 
  // background will too.

  return (
    <>
      <Button
        variant="contained"
        startIcon={"🥭"}
        onClick={() => {
          setBasket({ fruit: "mango" });
        }}
      >
        Set to mango
      </Button>
      <Button
        startIcon={"🍍"}
        variant="contained"
        onClick={() => setBasket({ fruit: "pineapple" })}
      >
        Set to pineapple
      </Button>
      <Button startIcon={"🔄"} variant="outlined" onClick={refresh}>
        Refresh bgcolor
      </Button>
      <Code mb={2}>
        {`{ fruit: "${basket?.fruit}" }`}
      </Code>
    </>
  );
}

About the authors & further reading

And that's a wrap! Thanks for sticking with it, you absolute rockstar 👩🏽‍🚀! Hopefully this has been a useful deep-dive into React memoization, and you're a little better equipped to tackle some of the dark magic rendering issues that await you in your own apps.

This interactive article was written by Brandon Shelley, with specical thanks to Serge Mugisha and Dina Buric for their invaluable work discovering, understanding, and documenting our experiences working with complex data at Delving.

Brandon ShelleyProduct designer · Software engineer

Victoria, BC · Canada

Brandon is a product design director and full-stack staff engineer with nearly two decades of experience building digital products for Fortune 500 companies and SaaS startups. He has a passion for product-led principles which foster engagement, retention, & user happiness, and champions mission, purpose, & innovation through focused execution.

Serge MugishaFull-stack developer · UI designer

Ottawa, Ontario · Canada

Serge is a full-stack software developer and UI designer. He is passionate about delivering innovative and effective solutions for established companies, startups, and individuals, has experience building software with React, Node, GraphQL, Python, and is proficient in Figma and other design tools.

Dina BuricFull-stack developer · Math artist

Victoria, BC · Canada

Dina is a full-stack developer based in Victoria, Canada, and has experience building software with React, Python, PHP, GraphQL, and PostgreSQL. She has has a Doctorate in Mathematics, has taught Algebra, Pre-Calculus, Calculus and Statistics, and is interested in machine learning algorithms and data science.


If you're interested in learning more, check out the following resources:

This site was built with:
and powered by: