Brett Thurston
Brett Thurston

Brett Thurston

Prevent re-renders with useRef

Prevent re-renders with useRef

Brett Thurston's photo
Brett Thurston
·May 25, 2022·

5 min read

Subscribe to my newsletter and never miss my upcoming articles

There may be times when you don't want to trigger renders when capturing data from the user. useState, by now, is a well known and handy hook since it was implemented in React 16.8. When setting our state variable with useState, it causes a render of your component. When we use useRef to persistently store information, it doesn't cause a render.

If you want to see the source code: https://github.com/BrettThurs10/useRefVersuseState

If you want to follow along in your browser: https://brettthurs10.github.io/useRef-vs-useState/

Dev note: The app is written in TypeScript, but only lightly so. If you're not use to TypeScript just ignore the parts that are unfamiliar, the business logic is the same. Having said that, now is a great time to learn TypeScript.

Dev note: As of React 18 components render twice by default if your is wrapped with . For this demo I've removed from the code base.

Jump to the RefComponent.tsx file and follow along:

Set the stage

To make the a ref simply import it and declare it as a variable:

import {useRef} from React;
...
  const dataRef = useRef("🥧");
  const inputRef = useRef<HTMLInputElement>(null);
  const timesRendered = useRef(0);
  const [inputString, setInputString] = useState("🍕");
...
}
export default RefComponent

I'm setting the pie emoji as the initial value for the dataRef constant. I'm also making a state variable called inputString and setting that to the pizza emoji.

Update your ref

Once you've declared the dataRef you can update it by assigning a value to it's property 'current'. This could be any primitive type, object or function.

In my method updateDataRef() this is where I'm doing just that.

const updateDataRef = (e: ChangeEvent<HTMLInputElement>) => {
    dataRef.current = e.target.value;
    console.log(dataRef.current);
  };

I then take the first input element and set the onChange attribute to that updateDataRef. Now whenever we type in it will take the value and update the ref for us.

I also make a handleOnChange() method to update the state variable stringInput for us, too.

  const handleOnChange = (e: ChangeEvent<HTMLInputElement>) => {
    setInputString(e.target.value);
  };

Likewise, I attach that to the 2nd input handling the inputString state variable. Whenever we type into that input element it WILL cause a re-render.

Monitor for changes to state

I've made the method whereFromMsg() to monitor from which useEffect code block the render is coming from. I put it into two useEffects that are listening to the dataRef and inputString variables to change.

  useEffect(() => {
    updateTimesRendered();
    renderMsg("dataRef useEffect");
    whereFromMsg("dataRef", dataRef.current);
  }, [dataRef]);

  useEffect(() => {
    updateTimesRendered();
    renderMsg("inputString useEffect");
    whereFromMsg("inputString", inputString);
    // uncomment to see how useRef can capture the previous state, but not current. i.e. typing in dog in the useState input you will see 'dog' and in the useRef value you will see 'do'
    // dataRef.current = inputString;
  }, [inputString]);

When they do, it will invoke 3 methods for me:

  • updateTimesRendered
  • renderMsg
  • whereFrom
 const updateTimesRendered = () =>
    (timesRendered.current = timesRendered.current + 1);

  const renderMsg = (fromWhere: string) => {
    console.log(
      `✨ Component has rendered ${timesRendered.current} times and most recently from ${fromWhere}`
    );
  };

  const whereFromMsg = (type: string, value: string) => {
    console.log(`${type} === ${value}`);
  };

Now we can see what is happening in the console.

App initialized

Whenever we type into either input we are seeing some message in console.

Typing into inputs shows console log messages

Notice when you type into the dataRef input, it only shows the value of dataRef.current. There is no message saying it's caused a render. Also notice how in the above screenshot the dataRef value in the UI is still set to the pizza emoji. That's because the component hasn't rendered yet. On any future render, it will update from pizza emoji to 'skateboard'.

Go ahead and type in the 2nd input and you'll see that transaction happen.

When we type into the inputString input we see a message it has rendered and the render counter increases in value.

InputString input shows console log

Keep things in sync

It's important to note that whenever we update a useRef variable our component UI won't know about it under another render happens.

You can see what the previous state for dataRef by uncommenting the dataRef.current = inputString line as shown below:

useEffect(() => {
    updateTimesRendered();
    renderMsg("inputString useEffect");
    whereFromMsg("inputString", inputString);
    // uncomment to see how useRef can capture the previous state, but not current. i.e. typing in dog in the useState input you will see 'dog' and in the useRef value you will see 'do'
    // dataRef.current = inputString;
  }, [inputString]);

Now, when we type into the 2nd input we see both values change, but the dataRef value is not current.

Screenshot showing values are not equal

This is because the ref will become current on a future render. But of course it may not be current with the inputString variable, should that update. Just to illustrate the point and help you keep things in sync. Use at your discretion.

Bonus points:

Clicking on the focus inputRef button will indeed set the 2nd input element to focus (drawing an outline around it). This is just shows how you can use the useRef hook and attach it to a DOM element to gain access to it directly.

Button focused

So next time you need to record some data without causing a re-render consider using useRef to help you out.

 
Share this