Alex's Notes

React Performance

Topic 8 in React Hooks: Tyler McGinnis

Recall the fundamental premise of React - UI is a function of your state UI = Fn(State)

Your function takes in arguments (props) and returns an object represetnation of your UI (JSX). When the state of the component changes, or your receive new props, the component re-renders, updating the UI.

In modern React, this is idea is taken to its logical conclusion - React components are just functions, literally. And like any function, code inside them will run every time the function is called.

Sometimes you want to opt out of that though, what if your computation is expensive? You don’t want to run it every time you render.

Take a scenario where you have state living high up in the component tree, it’s changed, causing a re-render of all the child components (the default behaviour). What if one of those children has an expensive render calculation. That will cause input lag potentially.

There is a higher order component, React.memo that will allow you to set behaviour on a component that it does not re-render if its props haven’t changed.

You can use it with React.memo(MyComponent).

Doing this causes react to memoize the result of rendering the component. Then it will do shallow comparisons of the props each time, if the props haven’t changed, then the memoized result will be used.

There’s a gotcha there, primitive values will work fine, but what about functions/references? They will never be equal since the reference of one function, object, array is not going to be equal to the reference of another.

We could do two things here.

You can pass a function as a second argument to React.memo to customize its behaviour, that function will receive the previous props and next props and should return a boolean describing whether you want to consider the props the same.

So if you just want to compare one value you can say: React.memo(MyComponent, (prevProps, nextProps) => prevProps.count === nextProps.count)

There’s another way to solve it though.

We can create functions or other objects whose references persist across renders.

There is a useCallback hook that will create a persistent reference like this. It looks like this:

const memoizedCallback = React.useCallback(
    () => doSomething(a,b),
    [a,b]
);

It takes a callback function (whose reference will persist), and an array of dependencies. The memoized function will only change if one of the values in the array changes. So you could use it like this for setting the state with a memoized function:

const incrementPrime = React.useCallback(
    () => setFibCount((c) => c + 1),
    []
);

Note that the dependency array is empty here, so this will have the same reference throughout the app lifetime.

There’s another approach too, you can memoize expensive computations within components. This is the useMemo hook. It looks a lot like useCallback

const memoizedValue = useMemo(
    () => computeExpensiveValue(a,b),
    [a,b]
);

So in a fibonnacci example it looks like this:

const fib = React.useMemo(
    () => calculateFib(count),
    [count]
);

So there’s three things to look at here:

React.memo - memoization at the component level.

React.useCallback - allows us to get referential consistency for functions across renders.

React.useMemo - allows us to memoize computations within components.

Often React.useMemo is the way to go, but note that React uses it as a hint, rather than a guarantee, so don’t rely on it to guarantee values persist across renders (useRef is made for that).

Note that the majority of the time it’s not necessary to memoize (hence the default behaviour).

Links to this note