DEV Community

Cover image for Prevent re-renders with useRef
Brett Thurston
Brett Thurston

Posted on

Prevent re-renders with useRef

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 state

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
Enter fullscreen mode Exit fullscreen mode

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);
  };
Enter fullscreen mode Exit fullscreen mode

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.

Macho Man Hulk Hogam GIF - Find & Share on GIPHY

Discover & share this Animated GIF with everyone you know. GIPHY is how you search, share, discover, and create GIFs.

giphy.com

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

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

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]);
Enter fullscreen mode Exit fullscreen mode

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}`);
  };

Enter fullscreen mode Exit fullscreen mode

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]);
Enter fullscreen mode Exit fullscreen mode

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.

Goofy Movie Hello GIF - Find & Share on GIPHY

Discover & share this Animated GIF with everyone you know. GIPHY is how you search, share, discover, and create GIFs.

giphy.com

Discussion (2)

Collapse
diballesteros profile image
Diego (Relatable Code)

There are some new hooks that can help out with the specific use case of inputs with React 18 like useDeferredValue. Regardless I don't think that was the point of the article. Thanks for sharing!

Collapse
brettthurs10 profile image
Brett Thurston Author

Thanks for the heads up! This is why I like to blog about stuff I learn - so I can learn more. 😎