React Hooks: Optimizing for performance

React Hooks: Optimizing for performance

If you have been using React hooks for a while, you might have already fallen in love with them. Almost all developers are now using them and changing their class based components into functional components as much as possible. This is good and is advocated by the React team. Functional components are just easy to understand and there is a lot less boiler plate code that needs to live in your code base.

If you have been using React in general for a while, you are well aware of the fact that, in production, developers try to optimize their components as much as possible. Components should not be rendered unnecessarily, so in class based components, lifecycle methods such as shouldComponentUpdate() were used to check some piece of state for a change. If the piece of state has changed, then React re-renders the component, otherwise it leaves it be.

How Re-render works in React

In React, when a parent component re-renders, all its child components re-render as a result (if no optimizations are implemented for the child components). A Component re-render can be triggered in a number of ways, a couple of which are:

  1. Change in the component’s state

  2. Change in the component’s props

Now what happens when all of your components are functional components? Remember, there are no lifecycle methods or hooks here to help you. Or are there?

Let’s see an example of a functional component with some state, re-rendering due to change in its state.


function App() {
  const [counter, setCounter] = useState(0);
  function formatCounter(counterVal) {
    return `The counter value is ${counterVal}`;
  }
  return (
    <div className="App">
      <div>{formatCounter(counter)}</div>
      <button onClick={() => setCounter(prevState => ++prevState)}>
        Increment
      </button>
  </div>
  );
}

This functional component has some state(counter) and upon clicking a button, the user is able to change it. Also notice the formatCounter() method, we'll get into it soon.

Let us take it step by step.

When the user clicks the button, we are calling the setCounter() method which will re-render the component with the new counter` state. In functional components, a re-render means that the whole function will be run again.

  1. So, starting from the top the useState() method will run. And due to the inherent nature of hooks, this will return the updated counter and the cached setCounter() method.

  2. Next, the formatCounter() function will be added to memory.

  3. Lastly, some JSX is returned. However, on the <button/>, there is an onClick() handler that is an arrow function. This function is gonna be added to the memory too.

Every time the component re-renders, all these steps will happen. As a good developer, you can not allow this. You need to somehow cache the formatCounter() and onClick() methods so they don't get added to memory on every re-render.

The Naive Solution

So the first thing that may come to mind is to move the formatCounter() and the onClick()handler out of the component. This way these functions are gonna be created only once.

Let's do that now.


function formatCounter(counterVal) {
  return `The counter value is ${counterVal}`;
}
function onClick(setCounter){
  setCounter(prevState => ++prevState)
}
function App() {
 const [counter, setCounter] = useState(0);
 return (
   <div className="App">
     <div>{formatCounter(counter)}</div>
     <button onClick={()=>onClick(setCounter)}>
       Increment
     </button>
 </div>
 );
}

The formatCounter() function is easy to extract. It doesn't depend on any component-specific variable or method.

This is not the case for the onClick()method though. It depends on the setCounter()method!

There is no simple way to extract this method. So it needs to exist inside the component and it needs to be cached so it doesn't get added to memory on every re-render.

useCallback()

Similar to useState() React provides a hook called useCallback(). The name might be confusing at first (as it was for me), but what it essentially does it that it takes a function and a dependency array that can contain variables and/or functions. If the variable or function's identity in the dependency array changes (shallow check), you're gonna get a new function, otherwise you're gonna get a cached function.


import React, { useState, useCallback} from "react";

function formatCounter(counterVal) {
  return `The counter value is ${counterVal}`;
}
function App() {
 const [counter, setCounter] = useState(0);
 const onClick = useCallback(()=>{
   setCounter(prevState => ++prevState)
 },[]);
 return (
   <div className="App">
     <div>{formatCounter(counter)}</div>
     <button onClick={onClick}>
       Increment
     </button>
 </div>
 );
}

When this component re-renders, both onClick()and formatCounter() are not entered into memory again, which is great.

Now if you do a performance test for this component, optimized vs un-optimized, you're gonna see a ridiculously small difference. However, in real-life projects, that have huge component trees, it becomes essential to cache event handlers and functions for making re-renders as fast as possible.

Imagine a component that has 15 components under it in the tree. If this component re-renders due to a state change or a prop change, all the unoptimized functions and event handlers in all those 15 components are going to be added to memory again. This, depending on the particular situation, might create jitters or stutters for the user while he's typing or clicking on the screen. In short, un-optimized functions add up.

ASIDE: Dependency Array

If you know the dependency array and how it works, then move on to the next section.

All you need to know about the dependency array is that if the *identity *of ANY of the variable/function/array/object in the array changes, you’re gonna get new stuff. In the case of the useCallback()hook, you're gonna get a NEW function and everything in that function, variables/functions/arrays/objects, will be re-initialized.

If you provide an empty array, you will always receive the same stuff that has been cached when the hook runs for the first time.

Using the correct dependencies are essential. And the people at React have released a linter that will let you know if you have missed something or have added something unnecessary. Here it is: eslint-plugin-react-hooks.

This comes pre-installed if you are using the new version of create-react-app ( versions 3.0.0 and higher)

But as a general rule of thumb, if there is some variable/function/object/array inside the stuff you provide to these optimization hooks (in the case of useCallback()its the function) that *NEEDS *to be the latest value, then you need to provide it as a dependency. There is an example later that shows this in action!

Are there any other optimizations?

function App() {
  const arrayOfNames = ["john", "mary", "doe", "doyle"];
  return (
    <div className="App">
      <div>{arrayOfNames}</div>
  </div>
  );
}

Sometimes, when you are initializing objects, arrays, and/or doing some synchronous heavy work (parsing, math calculation), you would want to optimize that as well. On every re-render, you can tell React to do the initialization/heavy work under certain circumstances only.

React provides another hook which is similar to useCallback()called useMemo().Instead of taking a callback function, it takes a normal function that returns a certain value. You should always be returning something from this function.

If you find yourself NOT returning something from this function, you might be wanting to use useCallback() instead.

The distinction between these two hooks become clear with some practice.

In the above example, arrayOfNames is an array that is initialized each time the component re-renders. In this simple example, the naive solution would be moving this initialization out of the component.However, if the initialization depends on the component itself, you are supposed to use the useMemo()hook and optimize it this way. Let's use useMemo()anyway for now.

import React, {useMemo} from "react";
function App() {
  const arrayOfNames = useMemo(()=> ["john", "mary", "doe", "doyle"],[]);
  return (
    <div className="App">
      <div>{arrayOfNames}</div>
  </div>
  );
}

Let’s now think of an example where the initialization does depend on the component itself.

import React, {useMemo} from "react";
import React, {useMemo} from "react";
function App({text}) {
 const [state, setState] = useState(true);
  const someRandomObject = {
      a: state ? 3 : 4,
      b: !!state
  };
  return (
    <div className="App">
      <div>{text}</div>
       <button onClick={()=> setState(!state)}> 
        Change 
      </button>
  </div>
  );
}

What’s interesting about this example is that the variable someRandomObject is totally dependent on the state. If the state changes, then this object *will *change.

For the people with a keen eye, this component is accepting a prop named text which is just being used in the JSX that is being returned.

Now imagine that for some reason, the prop text changes. What's gonna happen?

Well, the component will re-render right? What’s gonna happen to someRandomObject ?

Yes, you guessed it right. It’s gonna be re-initialized and added to memory. But does it have to? Did we *need *to re-initialize it? Of course not. Let’s use the useMemo()hook to optimize this.

import React, {useMemo} from "react";
function App({text}) {
 const [state, setState] = useState(true);
  const someRandomObject = useMemo(()=>{
      a: state ? 3 : 4,
      b: !!state
  },[]);
  return (
    <div className="App">
      <div>{text}</div>
      <button onClick={()=> setState(!state)}> 
        Change 
      </button>
  </div>
  );
}

And here you go. Your someRandomObject variable is now optimized and ready to go. Oh but wait...

If the state changes (by clicking the button), do you think that the optimized someRandomObject will reflect the correct value?

No it won’t.

Why?

If there is some variable/function/object/array inside the stuff you provide to these optimization hooks that *NEEDS *to be the latest value, then you need to provide it as a dependency.

I need the latest value of the state variable in order for my someRandomObject to be correct. If I don't provide the state variable as a dependency, someRandomObject will always be:


{
 a: 3,
 b: 1
}

This is the first time the variable was initialized right after the state variable was initialized to be true. After that, it will NEVER change.

In this case, we WANT it to update when the state variable changes. Hence, we just provide state as a dependency and all is good.

import React, {useMemo} from "react";
function App({text}) {
 const [state, setState] = useState(true);
  const someRandomObject = useMemo(()=>{
      a: state ? 3 : 4,
      b: !!state
  },[state]);
  return (
    <div className="App">
      <div>{text}</div>
      <button onClick={()=> setState(!state)}> 
        Change 
      </button>
  </div>
  );
}

So now if the component updates for whatever reason other than the setState() function being called (because that is the only time when state can change), someRandomObject will always return the cached value and it wont be added to memory every time, like when the prop text changes.

Verdict

I hope that you now have learned the importance of useCallback()and useMemo(), when to use them, how to handle the dependency array, and when to just take stuff out of the component itself.

I also invite you to do a mental exercise and think of the possible scenarios in which these optimization are necessary. It will help you gain perspective and help you understand the benefit of these hooks.

A word of warning before you go, do not optimize pre-maturely as it is the mother of all bugs. Write your components in the most simplest way possible first. Make sure all your tests run and that your components work as expected. ONLY then, move on to optimizations. Start with the basic ones.

Optimize the event handlers and initializations in components. See if you can take stuff out of the component.

Optimization should be an iterative process.

Optimize → check

Optimize → check

Optimize → check

.

.

.

Thanks for reading, and I hope this brought you guys value. I would love to hear your comments and things on which you maybe don’t agree.

UPDATE:

I got some feedback on Reddit that “this article does not give a good explanation of when this actually improves performance vs hurts it as well as code readability from premature optimization”.

I could see why this might be the case for some people, and I would like to elaborate on this point a bit as it is very important :)

Every line of code comes at a cost . Try to write your code without ANY optimizations and then start optimizing where necessary*

For example in cases where the user is typing something in an input field and some other part of the screen requires painting/updating. This might not be a problem for simple renders but if a lot of work is being done, then it’s better to optimize them.

In our company, I make sure that we are not optimizing any component until a problem appears or if we think that an incoming feature might affect the render time.

People tend to over-optimize and pre-optimize. I invite you to check out https://reacttraining.com/blog/react-inline-functions-and-performance where Ryan Florence talks about premature optimization.

Optimization hooks take up memory. For example, it instantiates a dependency array and then checks if something has changed or not in the dependency array every time there is a render. This is all WORK being done.

Thus, as a developer you have to decide whether the cost of the optimization hook is higher or lower.

Cheers!

2019-07-06