How to Add Caching to Your React App

Introduction

In modern web development, application speed and performance are critical. Users expect pages to load instantly, and developers strive to create the most responsive interfaces possible. One effective method for improving performance is data caching.

Caching can significantly reduce loading time and server load by storing frequently used data in local storage. In this article, we will look at one of the ways to cache in React applications – using hooks.

Let's implement caching technology into an existing React application

The essence of the application: on the page we will make a form in which the user will enter the ID of the Pokemon and there will be 3 possible options:

  1. The Pokemon will be found in the cache and its component will be loaded instantly using data from local storage

  2. The Pokemon will not be found in the storage. Then we will send a request to the server and give the user the opportunity to save the data to the cache

  3. Pokemon id will not be found in storage or on the server – we will send an error

Also, data that is stored in the cache for more than an hour (or any other time) will be deleted so as not to clutter the storage.

I will use it for API https://pokeapi.co/ – a mock API that is just right for our purposes.

I will also use the library reactuse. This is the largest set of reusable react hooks. Using this library, you can stop worrying about the default web functionality, because this work will be done for you by hooks from the package. From reactuse we will take 3 hooks: useLocalStorage, useQuery, useMount.

We install the library:

$ npm i @siberiacancode/reactuse --save
# или
$ yarn add @siberiacancode/reactuse

Let's create a layout for a form with an input element (to enter the Pokemon id)

import { useState } from "react";

function App() {
  const [inputText, setInputText] = useState("");
  const [pokemonId, setPokemonId] = useState("");
  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setInputText(event.target.value);
  };
  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    setPokemonId(inputText);
  };  return (
    <div>
      <form onSubmit={handleSubmit}>
        <label>Enter id of the pokemon </label>
        <input onChange={handleChange} type="text" />
        <button type="submit">Find Pokemon</button>
      </form>
    </div>
  );
}

Here we use useState to store the value from the input field in the inputText state. onChange update the value of this state to the current one. Also create a state pokemonId, if in the case of inputText we simply store any information that the user enters into the input, then here we already store the specific ID of the pokemon, by which we will send the request.

We create a handleSubmit function. It will be called when the form is submitted. In it we write that as soon as the form is confirmed, we will write the value from our input field to pokemonId. We use e.preventDefault() to prevent the page from reloading once the form is submitted.

Now let's create a new Pokemon component and accept the Pokemon id in the props

interface PokemonProps {
id: string
}
const Pokemon = ({id}:PokemonProps) => {
return <div>Pokemon!</div>;
};
export default Pokemon;

Let's go back to the main component and call Pokemon (only if the pokemonId state is not empty) and pass the pokemonId state to the props.

</form>
      {pokemonId && <Pokemon id={pokemonId} />}
      ...

Now we will work only with the Pokemon component. Here we need to implement the following logic:

  1. Find pokemon id in cache

    1. if it is found – render data from cache

    2. if it is not there – send a request to the server, get the data, and draw the write to cache button

  2. If more than a certain amount of time has passed – remove a certain Pokemon from the cache

Import hooks:

import { useLocalStorage, useMount, useQuery} from "@siberiacancode/reactuse";

Let's create a Pokemon interface

interface IPokemon {
  id: string;
  name: string;
  imageURL: string;
  expiresAt: number;
}

Let's create a constant that shows how many seconds after which data should be deleted from the cache.

const CACHE_EXPIRATION_TIME = 3600; // seconds

We use the useLocalStorage hook for the “pokemons” key. And immediately create a constant cachedPokemon in it we will store the Pokemon data from the cache, according to the requested ID (if it is there, of course)

const { value, set } = useLocalStorage<IPokemon[]>("pokemons");
const cachedPokemon = value?.find((p) => p.id === id);

We create a request to the API using useQuery:

const { data, isLoading, isError, error } = useQuery(
    () =>
      fetch(`https://pokeapi.co/api/v2/pokemon/${id}`).then((res) =>
        res.json()
      ),
    { keys: [id], enabled: !cachedPokemon }
  );

We make a request by ID, which we take from the props. Important: in the hook options we specify in keys: id, so that the request is sent again as soon as the prop id is updated. We also add enabled: !cachedPokemon, this can be translated as: “send request only if it was not possible to get Pokemon from cache”

Now we do a series of checks and render the ui

if (cachedPokemon)
    return (
      <div>
        <h1>{cachedPokemon.name}</h1>
        <h3>{cachedPokemon.id}</h3>
        <p style={{ color: "green" }}>Loaded from cache</p>
        <img src={cachedPokemon.imageURL} alt={cachedPokemon.name} />
      </div>
    );

If we managed to get the Pokemon from the cache, we will draw it (we will add in green text that the data was taken from the storage and is loaded in an optimized manner).

Error checking and data loading:

if (isLoading) return <h3>Fetching Pokemon...</h3>;
if (isError) return <h3>Error: {error?.message}</h3>;

And if we were unable to get the Pokemon and had to make a request to the server, then we will draw the Pokemon component, add the “Save in cache” button, and mark in red text that the data was taken from the server.

// In expiresAt we write the current time (Date.now() ) + constant CACHE_EXPIRATION_TIME , multiplied by 1000 (to represent seconds)

if (data && !cachedPokemon) {
    const fetchedPokemon: IPokemon = {
      id: String(data.id),
      name: data.name,
      imageURL: `https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/other/official-artwork/${data.id}.png`,
      expiresAt: Date.now() + CACHE_EXPIRATION_TIME * 1000,
    };
    return (
      <div>
        <h1>{fetchedPokemon.name}</h1>
        <h3>{fetchedPokemon.id}</h3>
        <div>
          <p style={{ color: "red" }}>Loaded from server.</p>
          <button
            onClick={() => {
              let arrayOfPokemons = value;
              arrayOfPokemons?.push(fetchedPokemon);
              arrayOfPokemons ? set(arrayOfPokemons) : set([fetchedPokemon]);
            }}
          >
            Save in cache
          </button>
        </div>
    &lt;img src={fetchedPokemon.imageURL} alt={data.name} /&gt;
  &lt;/div&gt;
);

}

When you click the button, we will create a separate local variable ArrayOfPokemons, equal to the current value of the state from the cache, we add to the array of the Pokemon requested from the server (fetchedPokemon) and we will place this array into storage, including both the previous state from the cache and the new Pokemon.

Now let's go back to the very beginning of the code, and before the useLocalStorage declaration, write the following:

useMount(() => {
    let arrayOfPokemons = value;
    arrayOfPokemons?.map((pokemon, index) => {
      if (pokemon.expiresAt <= Date.now()) {
        arrayOfPokemons?.splice(index);
      }
    });
    arrayOfPokemons && set(arrayOfPokemons);
  });

On the component mount, we will iterate through the array of Pokemon from the cache, and if any of the Pokemon is already expired (Date.now() ≥ expiresAt), then we will remove the Pokemon from the array.

That's all!

Demo of the application

All code

//App.tsx
import { useState } from "react";
import Pokemon from "./Pokemon";

function App() {
  const [inputText, setInputText] = useState("");
  const [pokemonId, setPokemonId] = useState("");
  const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
    setInputText(event.target.value);
  };
  const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    setPokemonId(inputText);
  };
  return (
    <div>
      <form onSubmit={handleSubmit}>
        <label>Enter id of the pokemon </label>
        <input onChange={handleChange} type="text" />
        <button type="submit">Find Pokemon</button>
      </form>

      {pokemonId && <Pokemon id={pokemonId} />}
    </div>
  );
}

export default App;

reactuse links

npmgithub

Similar Posts

Leave a Reply

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