Home > Net >  How can I stop a recursing function on click?
How can I stop a recursing function on click?

Time:10-07

So I have a function that fetches from an API until it hits a 200 code.

Something like this:

/* These functions are in a separate file */
const fetchFile = async () => {
 try {
   const resp = await fetch('someUrl');
   return resp.data;
 } catch (err) {
    if(err.response.code === 404){
      await sleep(10); //sleep is a custom fn that awaits for N seconds
      return fetchFile();
    }else{
      return 'Error';
    }
 }
}

const fetchAll = async (setData, setError) => {
  const data = await fetchFile();
  if(data === 'Error') {
    setError('Sorry, error ocurred');
    return;
  }
  setData(data);
}




/* React component */
const [data, setData] = useState([]);
const [error, setError] = useState('');
<button onClick={fetchAll}>Start fetch</button>

And I need to have another button that let's the user stop the recursing function. I know a common way of doing this is having a flag and check the value every time I call the recursing function, but since this is React, how can I achieve this? Should I have the functions inside the component file? Use a global window variable? Use localstorage? What's the best way?

CodePudding user response:

You can do couple of ways, here is one the way using useRef

/* These functions are in a separate file */

const fetchFile = async (cancellationRef) => {
 try {
   const resp = await fetch('someUrl');
   return resp.data;
 } catch (err) {
    if(err.response.code === 404){
       if(cancellationRef.current !==true){
         await sleep(10); //sleep is a custom fn that awaits for N seconds
         return fetchFile();
        }
        else{
           return 'Request cancelled';
        }
    }else{
      return 'Error';
    }
 }
}

const fetchAll = async (setData, setError,cancellationRef) => {
  const data = await fetchFile(cancellationRef);
  if(data === 'Error') {
    setError('Sorry, error ocurred');
    return;
  }
  setData(data);
}




/* React component */
const [data, setData] = useState([]);
const cancellationRef = useRef(false);
const [error, setError] = useState('');
<button onClick={fetchAll}>Start fetch</button>
  <button onClick={()=>cancellationRef.current =true)}>Stop looping</button>

CodePudding user response:

Here is a full example - I recommend encapsulating the fetching behavior in a hook like this. An AbortController is used for cancelling any ongoing request. This takes advantage of useEffect and its cleanup function, which is described in the react docs:

If your effect returns a function, React will run it when it is time to clean up:

/** Continuously makes requests until a non-404 response is received */
const fetchFile = async (url, signal) => {
    try {
        const resp = await fetch(url, { signal });
        return resp.data;
      } catch (err) {
         if(err.response.code === 404){
           await sleep(10);
           return fetchFile(url, signal);
         }
         // there was an actual error - rethrow
         throw err;
      }
}

/** Hook for fetching data and cancelling the request */
const useDataFetcher = (isFetching) => {
    // I advise using `null` as default values
    const [data, setData] = useState(null);
    const [error, setError] = useState(null);

    useEffect(() => {
        if (isFetching) {
            const controller = new AbortController();
            fetchFile('someUrl', controller.signal)
                .then(result => {
                    setData(result);
                    setError(null);
                })
                .catch(err => {
                    if (err.name === "AbortError") {
                        // request was aborted, reset state
                        setData(null);
                        setError(null);
                    } else {
                        setError(err);
                    }
                });

            // This cleanup is called every time the hook reruns (any time isFetching changes)
            return () => {
                if (!controller.signal.aborted) {
                    controller.abort();
                }
            }
        }        
    }, [isFetching]);

    return { data, error };
}

const MyComponent = () => {
    const [isFetching, setIsFetching] = useState(false);
    const { data, error } = useDataFetcher(isFetching);

    const startFetch = useCallback(() => setIsFetching(true), []);
    const cancelFetch = useCallback(() => setIsFetching(false), []);

    useEffect(() => {
        if (data || error) {
            setIsFetching(false);
        }
    }, [data, error];

    return isFetching
        ? <button onClick={cancelFetch}>Cancel</button>
        : <button onClick={startFetch}>Start fetch</button>
}
  • Related