useRef vs useState: Should we re-render or not?

Christian Nwamba

·

April 11, 2019

TL;DR

  1. useState causes re-render; useRef does not.
  2. Both useState and useRef remembers their data after a re-render


Getting started with React is not the hard part, it’s understanding React’s lifecycle and how re-rendering works. With these new Hooks API’s, we have an extra layer of question marks regarding when and how reconciliation in the virtual DOM tree happens (aka re-rendering).

Reconciliation is React’s attempt to re-render ONLY the changed diff of the DOM tree

There are two reasons why we need to know when re-rendering happens:

  1. Re-rendering updates the UI based on the latest changes
  2. Re-rendering triggers DOM tree reconciliation which is a performance factor

What causes re-rendering?

React re-renders to show us the changes we have requested through events, requests, timers, and so on. But they are not the actual triggers — state change is the actual trigger. This makes sense though, because events, requests, etc end up updating the state which in turn triggers a re-rendering process.

When components are nested, it becomes blurry to know how exactly a state is triggering a change. This is usually confusing to beginners.

function App() {
  const [value, setValue] = React.useState("");
  const handleInputChange = e => {
    setValue(e.target.value);
  };
  return (
    <div className="App">
      <Input value={value} onChange={handleInputChange} />
      <Button>Button</Button>
    </div>
  );
}

App has two children components Input and Button. What part of this tree do you think will re-render when the user enters a value in the input field?

const Input = ({ value, onChange }) => (
  <input value={value} onChange={onChange} />
);

We forget to account for nested components.

What confuses most beginners I mentor are nested components. They end up thinking that props trigger re-rendering too. What I usually hear is, “but Input has no state, just props; yet it is re-rendering, why?”

To prove that Input is actually re-rendering — Let’s take a look at React’s dev tool, click on the settings/gear icon, enable Highlight Updates


Then try to type in the input field:

Notice how App, Input and Button are all flashing, showing that the three components are re-rendering. And this re-render is triggered by just a change made to one component, Input

Now this is where the beginner starts thinking — “Oh! It’s not just the Input. The button also updates even without being passed any props.” These updates are not as a result of the input element changes. They are triggered by the value state in App which the input element updated.

What this means is that if the App’s state changes, then every child of App will re-render. The exception to this behavior is when you explicitly tell Input or Button not to re-render using PureComponent (read) or shouldComponentUpdate (read).

Apart from those exceptions, there are two other workarounds that help control re-rendering. The first one is to move state logic closer to the presentation component that triggers these updates — from App to Input:

const Input = () => {
  const [value, setValue] = React.useState("");
  const handleInputChange = e => {
    setValue(e.target.value);
  };
  return <input value={value} onChange={handleInputChange} />;
};

This way, App will stop updating as a result of state change since the state has been moved down to Input. App not updating also means that Button will stop updating.

The key take away here is — notwithstanding that reconciliation makes React fast, some complex DOM tree can cause performance bottlenecks. To avoid such, use the strategies I listed above and only use it when you have measured and confirmed that optimization is needed. You must have heard this a lot but please, do not blindly optimize.

Preserving data without re-rendering

State preserves data during changes and UI updates. Sometimes the UI updates might not just be worth it. Keystrokes are not enough reason for as atomic as an input element to update, let alone a component. This is why some people opt for uncontrolled components in React.

Uncontrolled components avoid preserving data in state but instead, preserve data in what React calls refs. The following example uses ref to handle forms:

function App() {
  const [value, setValue] = React.useState("");
  const valueRef = React.useRef();

  const handleClick = e => {
    setValue(valueRef.current.value);
  };

  return (
    <div className="App">
      <h4>Value: {value}</h4>
      <input ref={valueRef} />
      <Button onClick={handleClick}>Button</Button>
    </div>
  );
}

The trick is that there’s no state item in the value of input, hence NO re-render will happen when we start filling the form.

Controlled components are the opposites of uncontrolled. They are the components that use states for data preservation. Controlled components are NOT bad. In fact, they are the most recommended. You can see how I used the state hook to abstract form logic in this article and you can also read this article to learn the differences between controlled and uncontrolled.

useRef remembers

One take away is that useRef is not just good at preserving data. Its ability to preserve data also makes it powerful for remembering the state of timing functions like setInterval, debounce, throttle, etc. You can read this article by Dan regarding this.

Thinking out of the box

I have been doing a lot of research on what I like to call utility hooks — useRef, useCallback, useMemo, etc. In an attempt to better understand how they work, I have come to realize how powerful they. These findings led me to this first of three articles (others coming soon). I’ve come to understand that useRef is more powerful and useful than the class component based ref. In the next article, you will learn more about more powerful features of useRef.

Feel free to tweet at me, if I missed something or put something wrongly.


Christian Nwamba

Written by Christian Nwamba.
Follow on Twitter

© 2019, Codebeast.dev