skip to Main Content

I have a component that makes an API request to retrieve data from backend. The request has several dynamic parameters. If one of the parameters is changed then the API call repeats to retrieve new data.

Example:

const Component = ({dep1, dep2}) => {
  const [data, setData] = useState(null);

  const getData = async () => {
    const data = await api.getData({dep1, dep2});
    setData(data);
  }

  useEffect(() => {
    getData();
  }, [dep1, dep2])

  return <div>{JSON.stringify(data)}</div>
}

So my problem is that i have this situation:

  1. dep1 is changed, first request is sent
  2. dep2 is changed, second request is sent
  3. second request finished first, setState is called first time.
  4. first request finished second, setState is called second time.

So in the end i have data from the first request in my current state…
How can i avoid that? How to call setState in the same order that my requests are sent and not in the order of received responses?

Thank you

2

Answers


  1. Try this solution.

    const Component = ({ dep1, dep2 }) => {
      const [data, setData] = useState(null);
      const [requestCounter, setRequestCounter] = useState(0);
    
      const getData = async () => {
        const currentRequestCounter = requestCounter + 1;
        setRequestCounter(currentRequestCounter);
    
        const responseData = await api.getData({ dep1, dep2 });
        
        // Check if the response corresponds to the latest request
        if (currentRequestCounter === requestCounter + 1) {
          setData(responseData);
        }
      };
    
      useEffect(() => {
        getData();
      }, [dep1, dep2]);
    
      return <div>{JSON.stringify(data)}</div>;
    };
    

    If this not what you expected or if this solution do not work, please let me know.

    Login or Signup to reply.
  2. I’m going to assume you don’t really need the calls to be in order, but instead you just don’t want an earlier call’s result to overwrite a result from a later call.

    There are at least a couple of different ways to do it.

    1. Use an AbortController and abort it when the deps change; ideally, update api.getData so it use the AbortControllerAbortSignal (for instance, it could pass it to fetch if it uses fetch). Here’s an example — I moved getData into the useEffect as there’s no need to recreate the function every time:

      const Component = ({ dep1, dep2 }) => {
          const [data, setData] = useState(null);
      
          useEffect(() => {
              const controller = new AbortController();
              (async function getData() {
                  const { signal } = controller;
                  try {
                      // Get the data; if it supports accepting and respecting an `AbortSignal`, pass it
                      // the abort signal (if it doesn't, add that if you can)
                      //                                     vvvvvvvv
                      const data = api.getData({ dep1, dep2 }, signal);
                      // If the call hasn't been aborted, save the data
                      if (!signal.aborted) {
                          setData(data);
                      }
                  } catch (error) {
                      if (!signal.aborted) {
                          // ...handle/report errors...
                      }
                  }
              })();
              // In the cleanup function, abort any in-progress request
              return () => {
                  controller.abort();
              };
          }, [dep1, dep2]);
      
          return <div>{JSON.stringify(data)}</div>;
      };
      

      By making the AbortSignal available to getData, you may be able to prevent work involved in getting the data that would be wasted because of a newer call. But the above works even if for some reason you can’t make getData abortable.

    2. Use a sequence counter and only save the data if the counter matches the value you had when you started the request. You can track the counter in a ref so that the current value of it is accessible inside getData.

      const Component = ({ dep1, dep2 }) => {
          const [data, setData] = useState(null);
          // Use a ref to track a sequence number for calls to `setData`.
          const setDataRef = useRef(0);
      
          const getData = async () => {
              // Increment the counter and remember it
              const sequenceNumber = ++setDataRef.current;
              try {
                  const data = await api.getData({ dep1, dep2 });
                  // Only save the resulting data if nothing has incremented the counter.
                  // This works because the ref is stable and always refers to the  same
                  // object, so we'll see the current `current` value.
                  if (sequenceNumber === setDataRef.current) {
                      setData(data);
                  }
              } catch (error) {
                  if (sequenceNumber === setDataRef.current) {
                      // ...handle/report error...
                  }
              }
          };
      
          useEffect(() => {
              getData();
          }, [dep1, dep2]);
      
          return <div>{JSON.stringify(data)}</div>;
      };
      

    Other things being equal, I’d use #1, even if I couldn’t update getData (but particularly if I could).

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search