Toggling a recursive fetch request based on React state using `useEffect`

Viewed 582

I am trying to start and stop a recursive fetch request with a button in React. I would also like to have a delay between each request. For this, I decided to use the useEffect hook and link it to a boolean state that is controlled by the button. Whenever this state updates, the script inside useEffect checks if it is true or false. If it's true, I would like to fetch some necessary data and initiate a recursive fetch function. If it's false, it means the recursive function is already running thus the ongoing request should be cancelled, stopping the recursion, and the setTimeout used to achieve the delay between requests should be cleared.

The code below is a mockup of how I attempted to solve this problem. However, it doesn't work, probably for the following reasons:

  • The variables request and timeout are recreated on every rerender. I think this could be solved with the useRef hook; however, I'm a beginner with React and haven't managed to get it to work.
  • My functions seem to be not getting any state updates; for example, in recursiveFetch, data2 is always default when it should contain a new value set by the fetch request in useEffect.

I've tried many and many different combinations and ways to get this to work but didn't find any success. How should I approach this problem?

import { useState, useEffect } from 'react';

const Component = () => {
    const [executing, setExecuting] = useState(false);

    const [data1, setData1] = useState('default');
    const [data2, setData2] = useState('default');

    const [data3, setData3] = useState('default');

    // I think these variables should be defined with `useRef` but I'm not sure...
    let request;
    let timeout;

    const recursiveFetch = () => {
        request = fetch(`https://website.com/${data2}`, { method: 'POST' })
        .then((res) => {
            timeout = setTimeout(() => {
                recursiveFetch();
            }, 1000);
        }
    }

    useEffect(() => {
        if(executing){
            // Fetch some necessary data
            fetch(`https://example.com/${data1}`)
                .then((res) => res.json())
                .then((data) => {
                    setData2(data.exampleProperty);
                }
            // Call recursive function
            recursiveFetch();
        } else {
            // Cancel ongoing fetch request and clear timeout
            request.cancel();
            clearTimeout(timeout);
        }
    }, [executing])
    return (
        <>
            <Button onClick={() => setExecuting((prev) => !prev)}></Button>
        </>
    );
};

export default Component;
1 Answers

First of all, the recursiveFetch function isn't recursive since it doesn't actually call itself within its own stack frame. The call is made by an asynchronous function (the callback to setTimeout) that runs when the stack is cleared. See What the heck is the event loop anyway? for details on when asynchronous functions run.

Secondly, there's no need for repeatedly setting and clearing setTimeouts here. That's what setInterval is for. Since you're re-rendering anyway when executing changes, you can use that to conditionally toggle the interval.

Thirdly, don't forget to return a cleanup function from your useEffect that clears your timeout when the component unmounts.

I appreciate that you're experimenting for pedagogical purposes, but it bears mention that the patterns you're exploring aren't the sort of thing you'd see done in React, unless I'm misunderstanding what you're trying to achieve. Without much motivation for it, it seems best to provide a direct solution that seems to do basically what you're looking to do in a normal, un-fancy way:

const Example = () => {
  const [executing, setExecuting] = React.useState(false);
  
  React.useEffect(() => {
    if (executing) {
      const interval = setInterval(() => {
        console.log("fetched");
      }, 1000);
      return () => clearInterval(interval);
    }
  }, [executing]);
  
  return (
    <div>
      <p>check the browser console to see logs</p>
      <button onClick={() => setExecuting(prev => !prev)}>
        {executing ? "on" : "off"}
      </button>
    </div>
  );
};

ReactDOM.render(<Example />, document.body);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>

If you need to fetch some data whenever you begin the interval, you might try something like:

const Example = () => {
  const [executing, setExecuting] = React.useState(false);
  const [data, setData] = React.useState("default");
  
  React.useEffect(() => {
    if (!executing) {
      return;
    }
    
    fetch("https://httpbin.org/get")
      .then(res => res.text())
      .then(data => setData(data))
      .catch(err => console.error(err))
    ;
    const interval = setInterval(() => {
      console.log("fetched");
    }, 1000);
    return () => clearInterval(interval);
  }, [executing]);
  
  return (
    <div>
      <p>check the browser console to see logs</p>
      <button onClick={() => setExecuting(prev => !prev)}>
        {executing ? "on" : "off"}
      </button>
    </div>
  );
};

ReactDOM.render(<Example />, document.body);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>

If the interval shouldn't start until data is set (this appears to be your intent, because data2 is used in the URL), you can move it into the final then's callback in the fetch chain. Keep interval in the outer scope so it can be cleared and make sure not to start the interval if the component unmounted before the fetch response arrived.

If these don't match your use case exactly and aren't tweakable to meet your needs, I recommend providing more details to better motivate why you need to do something more complex.

Related