'React: How optimize a custom hook that shares data?

I have a Custom Hook similarly with the below:

import { useEffect, useState } from 'react';

import axios from 'axios';

const myCustomHook = () => {
  const [countries, setCountries] = useState([]);
  const [isLoading, setLoading] = useState(true);

  useEffect(() => {
      (async () =>
        await axios
          .get("MY_API/countries")
          .then(response => setCountries(response.data))
          .finally(() => setLoading(false)))();
  }, []);

  return countries;
};

export default myCustomHook;

The hook works great but I am using it in three different areas of my application despite the fact that all the countries are the same wherever the hook is used.

Is there a good pattern to call the axios request just once instead of three times?

EDIT - Final Code After Solution

import { useEffect, useState } from 'react';

import axios from 'axios';

let fakeCache = {
    alreadyCalled: false,
    countries: []
};

const myCustomHook = (forceUpdate = false) => {
  const [isLoading, setLoading] = useState(true);

  if (!fakeCache.alreadyCalled || forceUpdate) {
      fakeCache.alreadyCalled = true;

      (async () =>
        await axios
          .get("MY_API/countries")
          .then(response => setCountries(response.data))
          .finally(() => setLoading(false)))();
  }

  return countries;
};

export default myCustomHook;


Solution 1:[1]

One solution to this would be to introduce a custom "cacheing layer" (between your hook and the axios request) that:

  1. caches countries data returned from the first successful request and,
  2. return the same cached data on subsequent requests

There are a number of ways this could be implemented - one possibility would be to define a getCountries() function that implements that cacheing logic in a separate module, and then call that function from your hook:

countries.js

import axios from 'axios';

// Module scoped variable that holds cache data
let cachedData = undefined;

// Example function wraps network request with cacheing layer
export const getCountries = async() => {

  // We expect the data for countries to be an array. If cachedData
  // is not an array, attempts to populate the cache with data
  if (!Array.isArray(cachedData)) {  
    const response = await axios.get("MY_API/countries");

    // Populate the cache with data returned from request
    cachedData = response.data;
  }

  return cachedData;
}

myCustomHook.js

import { useEffect, useState } from 'react';
import { getCountries } from "/countries";

const myCustomHook = () => {
  const [countries, setCountries] = useState([]);
  const [isLoading, setLoading] = useState(true);

  useEffect(() => {

    (async() => {
      try {       
        setLoading(true);

        // Update hook state with countries data (cached or fresh)
        setCountries(await getCountries());

      } finally {
        setLoading(false)
      }
    }, []);
  });
}
export default myCustomHook;

Solution 2:[2]

Each instance of the component runs independently of each other.

Even though the 1st instance state has countries populated the 2nd instance is not aware of it and is still empty.

You could instead consume countries as a prop and if empty invoke the effect, otherwise just return the countries from props.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Hamza Hmem
Solution 2 Sushanth --