Caching with React

Ivan Montiel
4 min readAug 16, 2020

--

If you have an expensive calculation that you want to cache, there are a few different approaches you can take to cache that result using React and not have to constantly re-compute it. While you could wrap the expensive calculation using memoization, I’m going to cover a few techniques that move that responsibility to the component instead of the function.

By no means is this an exhaustive list, if you can think of others, please feel free to comment below.

State Hooks

A simple approach to caching synchronous (this one also applies to asynchronous calculations) would be to do the calculation when the component mounts or the required props change using useEffect. Once the expensive value is computed, you can store that value using the useState hook:

import React, { useState, useEffect } from 'react';
import expensiveCalculation from './expensiveCalculation';
const StateHooksExample = ({ propA, propB }) => {
const [expensiveValue, setExpensiveValue] = useState(0);
useEffect(() => {
setExpensiveValue(expensiveCalculation(propA, propB));
}, [propA, propB]);
return (
<div>{expensiveValue}</div>
);
};

A problem with using state here is that the setState in useEffect fires after the first render of the component. The user may see a flash of the default value for expensiveValue rendered.

Ref Hooks

If you want to only calculate an expensive value once when a component mounts and don’t care if the props change (this happens very rarely), then you can use refs to only get the value once and then use the ref to cache the result. Once set, if the component is re-rendered, it won’t be re-evaluated.

This is useful if you don’t want to memoize the value, but rather only want to calculate it once when the component is mounted. It also avoids the problem with using state where you will get an incorrect or loading value for the first frame.

import React, { useRef } from 'react';
import expensiveCalculation from './expensiveCalculation';
const RefHooksExample = ({ propA, propB }) => {
const expensiveRef = useRef(false);
if (expensiveRef.current === false) {
expensiveRef.current = expensiveCalculation(propA, propB);
}
return (
<div>{expensiveRef.current}</div>
);
};

The biggest problem here is that if propA or propB change, you'll have a stale value in expensiveRef, but it does avoid the initial render issue. If your expensiveCalculation does not rely on props, then this is a fairly safe approach.

Another noticeable problem is that using this technique will block the initial render of the component. You can see a significant delay in the Github repo linked to below. When mounted, this component blocks the initial render of the app until expensiveCalculation completes.

Memo

Another technique is to use memoization to cache the result of a component. If the component does not rely on any side-effects, you could cache the result of the component itself. The expensive calculation will only run once per unique set of props.

import React, { memo } from 'react';
import expensiveCalculation from './expensiveCalculation';
const MemoExample = memo(({ propA, propB }) => {
const expensiveValue = expensiveCalculation(propA, propB);
return (
<div>{expensiveValue}</div>
);
});

This is a very useful approach for straightforward components with no side-effects, but if the expensiveCalculation is impure (for example, it relies on the current datetime), then this method is not very helpful. Note that if other props that are passed in change, then the component will still continue to re-run the expensiveCalculation.

Similar to the ref example, is that using this technique will block the initial render of the component.

Dealing with Asynchronous Data

There is a lot already written about using fetch in and outside of React components, in this section, I'll be only looking at expensiveCalculations that may be asynchronous, and not necessarily network requests.

Suspense

An experimental feature in React makes it easier to cache values of asynchronous expensive calculations. You can wrap the async operation in a resource like below and use that resource to cache the promises and their results.

Using Suspense, you can wrap the component with fallback to render while the async operation is loading. The AsyncComponent below is similar to how we wrote our memo'd component in how simple it is.

The main complexity comes from the resource that we have to manage:

import React, { Suspense } from 'react';
import asyncExpensiveCalculation from './asyncExpensiveCalculation';
function expensiveResource() {
let suspenders = {};
return {
get(propA, propB) {
const key = `${JSON.stringify(propA)}${JSON.stringify(propB)}`;

if (!suspenders[key]) {
suspenders[key] = {
status: 'loading',
result: undefined,
promise: asyncExpensiveCalculation(propA, propB)
.then(data => {
suspenders[key].status = 'success';
suspenders[key].result = data;
}).catch(error => {
suspenders[key].status = 'error';
suspenders[key].result = error;
}),
};
}
switch (suspenders[key].status) {
case 'success':
return suspenders[key].result;
case 'error':
throw suspenders[key].result;
case 'loading':
default:
throw suspenders[key].promise;
}
},
};
};
const AsyncComponent = ({ resource, propA, propB }) => {
const asyncExpensiveValue = resource.get(propA, propB);
return (
<div>{asyncExpensiveValue}</div>
);
};
const resource = expensiveResource();const RenderAsyncComponent = () => {
return (
<Suspense fallback={<h1>Loading...</h1>}>
<AsyncComponent resource={resource} propA={propA} propB={propB} />
</Suspense>
);
}

Github Repo

The above examples can be viewed in the following Github repo:

Further Reading

--

--