How to Memoize Components in React

Using React.memo, useMemo other APIs to limit re-rendering of components

Memoizing in React is a performance feature of the framework that aims to speed up the render process of components. The technique is used in a wide spectrum of disciplines, from game engines to web applications. This article explores memoization techniques within React specifically, covering the APIs and example use cases along the way.

Memoizing is a well known concept in computer programming, aiming to speed up programs by caching results of expensive function calls and re-using those cached results as to avoid repeating those expensive operations:

Memoizing utilises a system’s memory to store results of expensive operations for subsequent usage.

While Memoizing often-times saves processing cycles, there is a limit to how much it can be used — this is of course dictated by a system’s memory limits. When it comes to React, we are caching the result of a component’s render() method — or simply the returned JSX of a functional component.

Memoizing can be applied to both class components and functional components; the feature is implemented has HOCs and React Hooks — both of which we’ll explore further down.

It is wise to consider how much of your application will be cached via memoizing. Even though I personally have not ran into memory limitations, mobile devices will inevitably have less memory to utilise than laptop or desktop counterparts.

Memoizing in React is not a guarantee that your components will be cached, but rather a best-effort attempt based on factors such as available resources.

There is no denying that fast and responsive UIs are great for the end user, and great for brand recognition with the experience delivered. A UI response delayed by over 100 milliseconds will already become perceptible to the end user. Aiming for 100 milliseconds or less for component re-renders, and UI feedback in general, is the ideal timeframe to keep the app feeling fluid.

Memoizing is just one technique to ensure this continues to be the case. Let’s explore further how Memoizing is implemented in React before looking at example use cases.

Introducing Memoizing with React.memo

Memoizing in React is primarily used for increasing rendering speed while decreasing rendering operations, caching a component’s render() result upon an initial render cycle, and re-using it given the same inputs (props, state, class properties, function variables).

To save these render() operations from repeating and generating an identical result, we can instead cache the result of the initial render() and refer to that result in memory the next time a component renders.

You may be thinking, React.PureComponent does this! React.PureComponent is indeed a performance optimisation, that implements the componentShouldUdpdate() lifecycle method to compare shallow props and state comparison from the previous render. If these match, the component will not re-render.

export class MyComponent extends React.PureComponent {
...
}

The term “shallow” is used to denote the props and state of the component being tested only. The props and state of child components are not tested with React.PureComponent.

React.PureComponent is limited to class components only, with its reliance on lifecycle methods and state. To remedy this, React introduced the React.memo API — a higher order component that implements the same shallow comparison on the component’s props to determine if a re-render will be processed. This HOC can then wrap functional components, too.

We can either wrap the API directly around the component:

const MyComponent = React.memo(function WrappedComponent(props) {
...
});

Or declare the component and React.memo separately:

function WrappedComponent(props) {
...
}
const MyComponent = React.memo(WrappedComponent);

React.memo also gives us the option to provide our own comparison function as a second argument to the API, giving us more granularity on determining whether a refresh is needed:

function myComparison(prevProps, nextProps) {
...
}
export default React.memo(WrappedComponent, myComparison);

It is common practice to refer to a component wrapped in a HOC as WrappedComponent. The memoized component is defined separately and exported as the default export.

Here are a couple of scenarios to consider on whether to implement a comparison function:

  • Perhaps we do not need every prop value to match with the previous render props. For example, if some props are passed down to child components for initialisation, these will not need to be compared on subsequent renders
  • In Reactive applications (Reactive design paradigm, not to be confused with the React framework) you may be listening to external services such as web socket events or an RxJS feed, and only wish to re-render a component when certain data is fetched from a server. Essentially, we can refer to global variables within myComparison so external factors can determine whether the component refreshes or not
  • If you’ve ran into a UI bug, it is simple to just return false from myComparison to temporarily override the memoization, forcing a refresh on every re-render and returning to the default component behaviour

Check out the following demo to see React.memo in action. We are randomly selecting an array of names and passing it down to a NameShuffling component.

The demo is available to clone on Github inside the <App /> component. The following screencast illustrates what is happening within this component,NameShuffling only re-rendering when the name prop changes:

Within the App component, we assign a random name to state via a getName() function:

// class property housing namesnames = [
'Alice',
'Bob'
];
// randomly select a namegetName = () => {
const index = Math.floor(Math.random() * (this.names.length - 1));
return (this.names[index]);
}

The name value in state is passed into the NameShuffling component. To update this value, the Shuffle button calls getName() and sets the name to App’s state.

NameShuffling is the component we are memoizing here. It is a functional component wrapped in React.memo:

import React, { memo } from 'react';function WrappedComponent (props) {
console.log('Re-rendering');
return (
<h2>Selected Name: {props.name}</h2>
);
}
export const NameShuffling = memo(WrappedComponent);export default NameShuffling;

We have imported memo from React here at the import statement, but we could have also just used React.memo. This is generally the developer’s decision to make.

We can demonstrate the comparison function here too, checking if the name prop is different from the previous render. We can also refer to global variables and external processes:

// some API outside the function scopeconst someGlobalVar = {
ready: 1
};
function areNamesEqual (prevProps, nextProps) {
return prevProps.name !== nextProps.name && someGlobalVar.ready === 1;
}

Here we are further limiting re-renders to when someGlobalVar returns a ready value of 1 and if the name prop has changed. If someGlobalVar was a server response, then the server would have control of when the component can re-render — useful for when the component is waiting for a complete list of data to be fetched before displaying all the results.

Memoizing components works well with functions as props too, provided that there are no prop or state dependencies in that function. We will cover how to deal with callback functions as props (state included) further down with the useCallback() Hook.

In the demo, a function is passed down to NameShuffling that clears the current name being selected. This will then trigger a re-render as the name will be changed:

// adding the ability to clear the name from stateclearName = () => {
this.setState({ name: null });
}
render() {
return (
...
<NameShuffling
name={this.state.name}
clearName={this.clearName}
/>
...
);
}

NameShuffling itself includes a Clear button, and either displays the currently selected name, or “None” if there is no name selected:

function WrappedComponent (props) {
return (
<>
<h2>Selected Name: {props.name ? props.name : 'None'}</h2>
<button
onClick={() => { props.clearName() }}>Clear
</button>

</>
);
}

As our component is Memoized, repeatedly clicking Clear will not cause more re-renders after the name is set to null.

This concludes are first basic demo. Let’s now dive into a more complex scenario where multiple memoization methods are used together.

The useMemo and useCallback Hooks

Memoization can also be done with Hooks and functional components, with a more flexible API than the React.memo counterpart that was built around components and props.

To be more specific, we have the ability to wrap inline JSX from a component’s return statement with useMemo, as well as storing Memoized results as variables. We can also memoize callback functions using the useCallback hook, an equivalent of useMemo with slightly different syntax.

useCallback solves the function-as-a-prop issue mentioned earlier, whereby a function is redefined upon a re-render if it is not memoized — we’ll check out a solution in the demo to follow.

The signature of useMemo demonstrates that it takes a function as its first argument, and an array of values — or dependencies — that trigger a re-render if their values change:

// useMemo signature const result = useMemo(() => computeComponentOrValue(a, b), [a, b]);

useCallback is used in conjunction with useMemo to memoize inline functions, and has a similar signature:

// useCallback signatureconst memoizedCallback = useCallback(
() => {
doSomething(a, b);
},
[a, b]
);

The difference between the two, as React documentation puts it, is that useMemo is used with “creation functions”, whereas useCallback is used for “inline functions”. This is not entirely clear on a first glance, so let’s dive back into the Name Shuffling demo and refactor our code, implementing these hooks in conjunction with React.memo.

The following solution is implemented within AppFunctional.js on Github, and is the default component that renders when running the project.

Let’s turn our attention back to the Name Shuffling example, this time implemented within a functional component.

  • useMemo will be used in a similar fashion to React.memo. Instead of wrapping a NameShuffling component with React.memo as we did previously, we will wrap the name display inline JSX with useMemo directly
  • useCallback will be used to memoize our getName() and clearName() methods. getName() will use the names array as its only dependency, only updating when names are added or removed from the list. clearName() will not have any dependencies, and will therefore be initialised and memoized until the component unmounts
  • We’ll wrap <Button /> components with React.memo to ensure they are not re-rendered. Our useCallback functions will be passed into these buttons
  • We will separate the names array into a top level component, and have our useCallback and useMemo hooks defined in a child component, separating the top level data to component logic

The following illustration breaks down this component structure:

Let’s look at some key points here that get Memoizing working smoothly.

// memoizing callback functionsconst getName = React.useCallback(
() => {
const index = Math.floor(Math.random() * (names.length));
setName(names[index]);
},
[names]);

const clearName = React.useCallback(() => {
setName(null)
}, []);

If we simply passed getName() and clearName() functions as props, they would have been recognised as different functions upon re-renders of the parent component — we are in essence redefining those functions upon a re-render of the component — which we do not want to do.

The useCallback hook solves this issue — the getName() function now only updates if names changes, being its only dependency. This makes sense, as names.length may be different, and the names array could contain different set of values. Beyond this scenario getName() will always stay the same — great use case for useCallback.

clearName() is also memoized, but has no dependencies to trigger an update to it. This is perfectly valid.

In this case, embedding useMemo() within the <Shuffle /> component’s return statement works well — we can clearly see the components we wish to memoize:

// inline JSX useMemo hookreturn (
...
{React.useMemo(
() => {
console.log('Name re-render');
return (
<h2>Selected Name: {name ? name : 'None'}</h2>
);
},
[name]
)}
...
);

We have opted to memoize the above <h2> element, and only re-render it if the current name changes.

Wrapping your JSX in <> and </>is the equivalent of using React.Fragment, allowing you to wrap JSX where there are more than 1 top level components. No markup is generated from fragments.

React.memo has been used again for the <Button /> components to perform a shallow prop comparison to determine whether they should be re-rendered. Because we are passing in our memoized callback functions (along with a primitive label string), there should not be any reason to re-render the buttons — the callbacks and labels will always stay the same.

The <Button /> component is simply a wrapped <WrappedButton /> component courtesy of the React.memo HOC. This is how it has been implemented:

function WrappedButton (props) {
console.log('button Re-render');
return (
<button
onClick={() => {
props.click();
}}
>
{props.label}
</button>
);
}
const Button = React.memo(WrappedButton);

The console.log statements are for debugging purposes to make it obvious in the console whether a re-render is taking place.

The full implementation can be found here on Github.

Memoizing Considerations

To use these APIs effectively, consider the following points to ensure your apps remain bug free while leveraging the performance gains of Memoizing:

  • Side effects (anything that affects something outside the scope of the function being executed) should not be performed in a memoized function. Leave this to the commit phase of the component, after its render phase has completed execution
  • Memoizing is performed in the render phase of a component, and side effects should typically be used in componentDidMount, componentDidUpdate, and componentDidCatch with class components, or useEffect with functional components
  • Use the Profiler to record the performance of your app before and after Memoizing. If the performance gains are not considerable, it may be worth removing the added complexity Memoization brings in certain areas of your app where there is not a huge performance gain, freeing up more memory and decreasing boilerplate
  • Compartmentalise a component into child components to leverage memoization if you need to, such as in forms whereby static components such as buttons, labels and icons may be combined with dynamic components that need re-rendering, such as with validation. Separate these and memoize where possible — this is especially useful with complex SVG icons that contain a lot of markup

This has been an introduction to Memoizing in React, using some of the available APIs designed to speed up re-rendering performance of your apps. The latter demo in-particular is a good reference point into using the talked about methods of memoizing — enjoy increasing the speed and fluidity of your apps!

Programmer and Author. Director @ JKRBInvestments.com. Creator of LearnChineseGrammar.com for iOS.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store