Exploring Caching Techniques in React

How to use memoization, contexts, useMemo, useState, and useEffect

For prospective students on the course “React.js Developer” prepared a translation of the material. We also invite everyone to an open webinar. “ReactJS: a quick start. Advantages and disadvantages”.


What we will create today!  Photo - the author of the article
What we will create today! Photo – the author of the article

Collecting data in React is one thing. Storing and caching this data is a different story. The possibilities seem endless, and the differences are often subtle, making choosing the right technique a little difficult at times.

Today we will explore various techniques and consider all their details and subtleties. Should I use useMemo or memoization? Should I store data with useState and context? When we’re done, you should be able to make an informed choice about data caching. You will learn about all the intricacies.

And a lot of animated GIFs. What more could you ask for?

Let’s start!

Our data

Before we dive into the code, we can take a quick look at the data that we will be pulling from (most) of our components. The file that acts as our API looks like this:

export default function handler(req, res) {
  setTimeout(
    () =>
      res.status(200).json({
        randomNumber: Math.round(Math.random() * 10000),
        people: [
          { name: "John Doe" },
          { name: "Olive Yew" },
          { name: "Allie Grater" },
        ],
      }),
    750
  );
}

This code is executed when we make a request to the / api / people path in our project. As you can see, we have returned an object with two properties:

  • randomNumber: a random number in the range 0-10000.

  • people: a static array with three fictitious names.

Property randomNumber will help us visualize whether we are displaying cached data in the frontend or not. Just keep reading. It will be clear soon.

Note that we are simulating small network latency with setTimeout

People component

When we display data from our API, we pass it to a component called PeopleRenderer… It looks like this:

With all of the above, let’s take a look at the first solution below.

… … …

useEffect

Inside our components, we could use a hook effect useEffect Hook to get data. We can then store them locally (inside the component) using useState :

import { useEffect, useState } from "react";
import PeopleRenderer from "../PeopleRenderer";

export default function PeopleUseEffect() {
  const [json, setJson] = useState(null);

  useEffect(() => {
    fetch("/api/people")
      .then((response) => response.json())
      .then((data) => setJson(data));
  }, []);

  return <PeopleRenderer json={json} />;
}

When passing an empty array as the second parameter (see line 11), useEffect Hook (hook effect) will be executed when our component is installed in the DOM (Document Object Model) – and only after that. It will not run again when our component is reloaded. This is called a “run once” Hook.

Limitation of use useEffect in this case is that when we have multiple instances of a component in our DOM, they will all receive data separately (when set):

There is nothing wrong with this method. Sometimes, this is exactly what we want. But in other cases, we may need to get the data once and reuse it in all other cases by caching. We can use several ways to achieve this.

… … …

Memoization

Memoization is a fancy word for a very simple technique. This means that you create a function, and each time it is called again, it stores the results in a cache to prevent recalculation.

When you first call this memoized function results are calculated (or retrieved, whatever you do inside the function body). Before returning the results, you store them in a cache under a key that is created with the input parameters:

const MyMemoizedFunction = (age) => {
  if(cache.hasKey(age)) {
    return cache.get(age);
  }
  const value = `You are ${age} years old!`;
  cache.set(age, value);
  return value;
}

Generating template code like this can quickly become cumbersome, which is why popular libraries like Lodash and Underscoreprovide utilities that you can use to easily create memoized functions:

import memoize from "lodash.memoize";

const MyFunction = (age) => {
  return `You are ${age} years old!`;
}
const MyMemoizedFunction = memoize(MyFunction);

Using memoization to retrieve data

We can use this technology when receiving data. Create a function getDatawhich returns Promiseavailable at the end of the data retrieval request. Memoize an object Promise:

import memoize from "lodash.memoize";

const getData = () =>
  new Promise((resolve) => {
    fetch("http://localhost:3000/api/people")
      .then((response) => response.json())
      .then((data) => resolve(data));
  });

export default memoize(getData);

Please note that we did not work with errors in this example. This deserves a separate article, especially when we use memoization (also memoized) rejected Promise which can be problematic).

Now we can replace our useEffect Hook to another that looks like this:

import { useEffect, useState } from "react";
import getData from "./getData";
import PeopleRenderer from "../PeopleRenderer";

export default function PeopleMemoize() {
  const [json, setJson] = useState(null);

  useEffect(() => {
    getData().then((data) => setJson(data));
  }, []);

  return <PeopleRenderer json={json} />;
}

Since the result of getData is memoized, all of our components will receive the same data when they are mounted:

Animation: Our components use the same memoized Promise.
Animation: Our components use the same memoized Promise.

It is also worth noting that the data is already pre-collected when we open the page memoize.tsx (before we mount the first version of our component). This is because we have defined our function getData in a separate file that is included at the top of our page, and the Promise is generated when this file is loaded.

We can also invalidate, invalidate (empty) the cache by assigning an entirely new Cache as the cache property of our memoized function:

getData.cache = new memoize.Cache();

Alternatively, you can clear the existing cache (using the function Map):

getData.cache.clear();

But this is Lodash specific functionality. Other libraries require different solutions. Here you can see invalidating the cache in action with an example:

Animation: Resetting the memoized cache of the getData function.
Animation: Resetting the memoized cache of the getData function.

… … …

React Context (React Context)

Another popular and widely discussed (but often misunderstood) tool is React Context… And on a note, once again, he not replaces tools like Redux. It is not a state management tool.

Mark erikson is fighting a tough battle on the Internet and goes on to explain why this is so. I highly recommend reading his last article about this theme.

And if you are really interested, read my article on this topic as well:

So what is Context? It is a mechanism for injecting data into your component tree. If you have any data, you can store it, for example, with useState Hook inside a component high in the component hierarchy. You can then inject the data into the tree using the Context Provider, after which you can read (use) the data in any of the components below.

This is easier to understand with an example. First, create a new context:

import { createContext } from "react";

export const PeopleContext = createContext(null);

Then we wrap the component that renders your People components with a Context Provider:

export default function Context() {
  const [json, setJson] = useState(null);

  useEffect(() => {
    fetch("/api/people")
      .then((response) => response.json())
      .then((data) => setJson(data));
  }, []);

  return (
    <PeopleContext.Provider value={{ json }}>
        ...
    </PeopleContext.Provider>
  );
}

On line 12, we can do whatever we want. At some point, further down the tree, we will display our People component (s):

import { useContext } from "react";
import PeopleRenderer from "../PeopleRenderer";
import { PeopleContext } from "./context";

export default function PeopleWithContext() {
  const { json } = useContext(PeopleContext);

  return <PeopleRenderer json={json} />;
}

We can use the value from Provider with useContext Hook. The result looks like this:

Animation: Using data from the Context.
Animation: Using data from the Context.

Pay attention to one important difference! At the end of the animation above, we click the “Set new seed” button. In this case, the data that is stored in our Context Provider will be retrieved again. After that (after 750 ms), the newly received data becomes the new value of our Provider, and our People components are reloaded. As you can see, they are all use the same data

This is a big difference from the memoization example we looked at above. In that case, each component kept its own copy of the memoized data using useState. And in this case, using and consuming the context, they do not store copies, but only use references to the same object. Therefore, when we update the value in our Provider, all components are updated with the same data.

… … …

useMemo

Last but not least, this is a quick look at useMemo… This Hook differs from the other methods we have already looked at in the sense that it is just a form of caching locally: within a single element of a component. You cannot use useMemo to communicate between multiple components – at least without workarounds like prop-drilling or dependency injection (like React Context).

useMemo is an optimization tool. You can use it to prevent the value from being recalculated every time you reuse your component. Documentation describes this is better than I can, but let’s see an example:

export default function Memo() {
  const getRnd = () => Math.round(Math.random() * 10000);

  const [age, setAge] = useState(24);
  const [randomNumber, setRandomNumber] = useState(getRnd());

  const pow = useMemo(() => Math.pow(age, 2) + getRnd(), [age]);

  return (
    ...
  );
}
  • getRnd (line 2): a function that returns a random number in the range 0-10000.

  • age (line 4): store a number representing age from useState

  • randomNumber (line 5): store a random number with useState

Finally, we use useMemo on line 7. We memoize the result of the function call and store it as a variable called pow. Our function returns a number that is the sum of age, increased to two, plus a random number. It has a dependency on the variable age, so we pass it as a dependent argument to the call useMemo

The function is executed only when the age value changes. If our component is re-rendered and the age value does not change, then useMemo will simply return a memoized result.

In this example, calculating pow is not very difficult, but you can imagine the benefits of it, given that our function is getting heavier and we often have to re-rendered our component.

The last two animations will show you what’s going on. First, we update the randomNumber component and leave the age value unaffected. So we see useMemo in action (the pow value does not change when the component is re-rendered. Every time we click on the button “randomNumer”, our component is re-rendered to its original value:

Animation: Many re-renders, but pow is memoized with useMemo.
Animation: Many re-renders, but pow is memoized with useMemo.

However, if we change the value age, pow receives a re-rendered response because our useMemo the call has a value dependency age:

Animation: Our memoized value is updated when the dependency is updated.
Animation: Our memoized value is updated when the dependency is updated.

… … …

Conclusion

There are many techniques and utilities for caching data in JavaScript. This article is only a superficial knowledge, but I hope it contains some information that you will take with you on the path of your development.

All the program code used in this article can be found in my archive at Gitlab

Thanks for your time!


Learn more about the course “React.js Developer”

Watch an open webinar “ReactJS: a quick start. Advantages and disadvantages”.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *