Memoize API response using useCallback hook

Viewed 4919

I need to fetch an API in my React project when I click on a button, but if I click again it should not redo the API call but retrieving the latest fetched data instead.

I know the hook useCallback that can memoize function. So I just put my API call inside an useCallback:

  const memoizedApiResponse = React.useCallback(async () => {
    console.log("fetch is called again :(");
    let response = await fetch("https://api.stackexchange.com/2.2/users?order=desc&sort=reputation&site=stackoverflow");

    return await response.text();
  }, []);

So I just call this memoized function when I click on the button:

import React from "react";

const App = () => {
  const [apiResponse, setApiResponse] = React.useState(undefined);

  const memoizedApiResponse = React.useCallback(async () => {
    console.log("fetch is called again :(");
    let response = await fetch(
      "https://api.stackexchange.com/2.2/users?order=desc&sort=reputation&site=stackoverflow"
    );

    return await response.text();
  }, []);

  const updateApiResult = async () => {
    const apiResponse = await memoizedApiResponse();
    setApiResponse(apiResponse);
  };

  return (
    <div className="App">
      <button onClick={updateApiResult}>fetch API</button>
      <p>{apiResponse}</p>
    </div>
  );
};

export default App;

But sadly on each click the request is send (you can see the message in the console). Have you an idea how to memoize the request response using useCallback?

Here is a link to the codesandbox.

2 Answers

memoizedApiResponse is not what its name implied (a memoized API response), it is a memoized function that will get you data from an API. You are just saving yourself the creation of the function that will get you the data, not the actual retrieval of the data.

Additionally, that useCallback probably isn't saving you much. I suggest you read this article: When to useMemo and useCallback it really helps with understanding what some of these memoizing hooks do and when they actually help you.

I think you want this

import React from "react";

const App = () => {
  const [apiResponse, setApiResponse] = React.useState(undefined);
  
  const getData = async () => {
    if (apiResponse) {
      return apiResponse;
    }
  
    const response = await fetch("https://api.stackexchange.com/2.2/users?order=desc&sort=reputation&site=stackoverflow");
    const data = await response.text();
    setApiResponse(data);
  };

  return (
    <div className="App">
      <button onClick={getData}>fetch API</button>
      <p>{apiResponse}</p>
    </div>
  );
};

export default App;

useCallback doesn't memoize responses, it memoizing the function declaration between renders.

In your case you only need to check if you already have a result, to fix your code and keep the useCallback you need to use a reference:

const textRef = useRef(null);

const memoizedApiResponse = React.useCallback(async () => {
  if (textRef.current === null) {
    console.log("fetch is called");
    textRef.current = true;
    let response = await fetch("...");

    const text = await response.text();
    textRef.current = text;
    return text;
  }

  return textRef.current;
}, []);

Edit cranky-silence-rdyxx

Related