Understanding re-rendering and memoization in React

Kolby Sisk
Udacity Eng & Data
Published in
4 min readDec 6, 2021

--

Photo by Salvatore Andrea Santacroce on Unsplash

The following are quick tips to increase performance in your React application by understanding re-rendering and memoization. To better understand the following tips it’s important to know when React re-renders components. Generally, a re-render is caused by a component’s props or state changing. When a component re-renders all of its children components will also re-render, unless they are memoized. (There are exceptions, but this is mostly true.)

useRef

A common mistake React devs make is utilizing useState for every mutable value they need to persist between renders. useState is a good solution if the rendered output depends on the value, otherwise useRef would be a more optimal solution.

Consider the following example:

const [firstName, setFirstName] = useState();
return (
<form onSubmit={() => alert(firstName)}>
<input onChange={(e) => { setFirstName(e.target.value) }} />
<button>Submit</button>
</form>
);

In this example every time the user types, the firstName state is updated. When a state is updated a re-render is triggered, meaning a re-render is happening every time the user types.

Since firstName isn’t being used in the rendered output we can replace it with useRef, and prevent the re-rendering.

const firstName = useRef();
return (
<form onSubmit={() => alert(firstName.current)}>
<input onChange={(e) => { firstName.current = e.target.value}}/>
<button>Submit</button>
</form>
);

memo

One of the most important concepts to understand for optimizing React is memoization. Memoization is the process of caching the results of a function, and returning the cache for subsequent requests.

Re-rendering a component simply means calling the component’s function again. If that component has children components it will call those components’ functions, and so on all the way down the tree. The results are then diffed with the DOM to determine if the UI should be updated. This diffing process is called reconciliation.

Since components are just functions though, they can be memoized using React.memo(). This prevents the component from re-rendering unless the dependencies (props) have changed. If you have a particularly heavy component then it is best to memoize it, but don’t memoize every component. Memoization uses memory and can be less performant in certain cases.

When a component is memoized, instead of re-rendering it, React diffs the component’s new props with its previous props. The trade off that needs to be considered here is how intensive it is to compare the props vs running the function. If you have a large object in your props, it could be less performant to memoize that component.

const HeavyComponent: FC = () => { return <div/>}
export const Heavy = React.memo(HeavyComponent);

ℹ️ Use React.memo() wisely.

useCallback

An important tool to prevent components that are memoized from re-rendering needlessly is useCallback. When passing a function into a memoized component you can unknowingly remove the memoizing effect by not memoizing that function using useCallback. The reason for this is referential equality. As mentioned previously, every re-render calls a component’s function. This means if we’re declaring a function in the component, a new function is created every re-render. If we are passing that function as a prop to another component, even though the contents of the function do not actually change, the reference changes which causes the child component to re-render, even if it is memoized.

export const ParentComponent = () => {
const handleSomething = () => {};
return <HeavyComponent onSomething={handleSomething} />
};

In this example every time ParentComponent is re-rendered, HeavyComponent will re-render as well, even though it is memoized. We can fix this by using useCallback and prevent the reference from changing.

export const ParentComponent = () => {
const handleSomething = useCallback(() => {}, []);
return <HeavyComponent onSomething={handleSomething} />
};

ℹ️ It is important to know when to use useCallback, and when not.

useMemo

By now we know that every re-render means the component’s function is getting called again. This means if your Component Function includes a call to an expensive function — that expensive function is being called every re-render. To avoid running the expensive function every re-render you can memoize it. The first render will call the function, and following re-renders will return the cached results of the function, rather than running it again.

The useMemo hook makes implementing memoization very simple:

const value = useMemo(() => expensiveFunction(aDep), [aDep]);

In our example value will be cached, and only updated when aDep changes.

ℹ️ It is important to know when to use useMemo, and when not.

useState lazy initialization

A lesser known feature of useState is the ability to lazily set the initial state. If you pass a function to useState it will only call the function when the component is initially rendered. This prevents the initial value from being set on every re-render, which is useful when your initial state is computationally heavy. If your initial value is not computationally heavy, lazy initialization is not recommended.

const initialState = () => calculateSomethingExpensive(props);
const [count, setCount] = useState(initialState);

ℹ️ Read more about useState lazy initialization

Conclusion

I know there are hundreds of articles that cover this topic, but my hope is that this article is concise enough that it helps someone when the others wouldn’t. If you’re interested in a more complete breakdown, check out Mark Erikson’s complete guide to React re-rendering.

If this article was helpful to you, or if you know any tips to optimize performance in React, leave a message and let me know. 🙂

--

--

Builder of software with a passion for learning. I specialize in web development and user experience design. www.kolbysisk.com