Stable State: Reducing React's Relentless Rendering
Tags: webdev, javascript, react
Published: 2024-04-05 22:19:02

React hooks removed a lot of code complexity, yet they introduce new challenges that can cause unexpected render cycles and difficult to diagnose defects as application complexity increases.

## Render Repetition

React is designed to _render many times_ and only update the page when necessary. This is intentional, but it means your code may run more frequently than expected. There is a difference between when a component is rendered and _the version you see_, but we'll get to that. First, I wan to quibble about terminology.

## React Redefines "Render"?

Each run of a ~~function~~ React component is a "render", but not all of these _function calls_ cause a display update, which can be confusing. The term "render" is used because it is creating DOM nodes, a bit like offscreen rendering in graphics. In React, "render" means a _function call_, and "update", "commit", or "paint" are used for placing content on the DOM.

## Hooks

Before hooks, Pure Functional Components were used for simple, stateless components. These could be safely run many times without risk.

```jsx
const ListItem = ({ id, label }) => (<li key={id}>{label}</li>);
```

Hooks hide the overhead of state management – similar to the instances Class Components used to be explicitly create – but they don't change the nature of React. It still calls components more often than the screen updates might suggest.

## The Hook Brings You Back

With `useState`, setting a new value will cause the component to be called again. Setting _the same value_, however, only _might_ trigger a call. The [useState Reference Docs mention the result may be ignored](https://react.dev/reference/react/useState#ive-updated-the-state-but-the-screen-doesnt-update), but there are still sometimes "extra" calls.

These seem to happen if any "conditions" – pieces of component state – are different from the last action. After changing any state value, the next set action will cause the component to render, even if the action and outcome are the same.

Let's see the calls and display cycles in action in the example below. You can set the object to `[0]`, or to an array containing the primitive state value, represented by the `[x]` button.

{% codepen https://codepen.io/_sir/pen/VwNMvKL %} 

Notice repeatedly setting the same primitive does not keep incrementing the the `callCount` – beyond the "conditions changed" call, anyway. In contrast, every set of the object causes a call and an update. This is because the object value is a different object each time, while the primitive value is strictly equal.

In the example, the `callCount` used to provide the Display Cycle and Call Count is tracked outside the component, but `useRef` can provide the same experience if you prefer.

## Noise is Nothing New

Though none of this should be shocking, it might not be well known by everyone. Even the [React Tutorial](https://react.dev/learn/updating-objects-in-state) recommends creating new objects in state but doesn't talk about equivalence. We might assume state is only set when it changes, which makes sense in an example or simple application, but there are reasons you might get repetitive or identical values, from API calls to timers.

As we add effects, requests, and complexity, we can encounter defects that can be difficult to trace.

### Maybe Memo Matters 

Built-in methods like [`memo`](https://react.dev/reference/react/memo) can help avoid extra calls, but even the docs caution against relying on it:

> This memoized version of your component will usually not be re-rendered when its parent component is re-rendered as long as its props have not changed. But React may still re-render it: memoization is a performance optimization, not a guarantee.

## Stable State Storage

We can try to prevent unnecessary updates, which is what we've done at my job for our simple state objects.

We created a small hook to ensure that state objects are not updated unnecessarily. This is done with a _comparator_ or a function that compares two values to see if they are equivalent. We use `react-fast-compare` for this, though our hook accepts alternatives. We call it `useStableState`.

### useStableState

```jsx
import isEqual from 'react-fast-compare';

const useStableState = (initial, comparator = isEqual) => {
  const [state, setState] = useState(initial);
  const setStable = useCallback((next) => {
    const nextFunction = typeof next === "function";
    setState((prev) => {
      const update = nextFunction ? next(prev) : next;
      return comparator(prev, update) ? prev : update;
    });
  }, [comparator]);
  return [state, setStable];
};
```

This hook has three key principles:

1. Match the `useState` design and support a value or function argument.
2. Ensure we don't keep recreate the "setter" function with `useCallback`.
3. Return the _previous_ state, not the new one, when the comparator returns `true`.

That last point is crucial to how the hook works. If the objects are considered to be the same, returning the existing state object avoids a state update and an unnecessary call.

Try it yourself.

{% codepen https://codepen.io/_sir/pen/MWREOPZ %} 

## Other Object Options

We encountered similar problems when accessing objects from Redux, and implemented a hook called `useComputedSelector`. While `useSelector` already supports an equality function (comparator), we found it doesn't always respect the result.

Extracting individual primitive values with multiple `useSelector` hooks is another solution, but can significantly impact the code readability when dealing with large objects. Additionally there can be performance considerations in limited cases. _Every useSelector is called when the Redux state changes_ because they are triggered by the Context provider update.

### useComputedSelector

```jsx
const useComputedSelector = (selectorFn, equalityFn = isEqual) => {
  const previousValue = useRef();
  return useSelector((state) => {
    const newValue = selectorFn(state);
    if (!equalityFn(previousValue.current, newValue)) {
      previousValue.current = newValue;
    }
    return previousValue.current;
  });
};
```

## Asking About Alternatives

We created these simple solutions to reduce unwanted renders in a complex application with many layers of nested state, but I'm interested to hear the changes, proposals, and solutions you use to prevent unnecessary state updates and reduce the amount of work React has to do. Thanks for reading!