1

Cancelling a request is super easy with useEffect's cleanup

useEffect(() => {
  let ignore = false;
  fetchData(id).then(data => {
    if (!ignore) {
      setData(data);
    }
  });
  return () => (ignore = true);
}, [id]);

I want to do something similar but I need to poll for the data with useInterval

I want to poll for data with fetchData(id) and ignore the returned response if the request had fired but the id changed before the response resolved.

5
  • 1
    I think you should abort the old request not ignore the response. Take look on AbortController Commented Oct 6, 2019 at 15:57
  • @MateuszKrzyżanowski what profit do you see in cancelling request over ignoring response? Commented Dec 26, 2019 at 18:27
  • I'm not sure if I got the idea. You want regularly fetch data for the same id? if yes, what should happen if response did not come in time and you have to send new request(what to do with potential race conditions)? Or do you want debounce fetching to save some server's CPU? Commented Dec 26, 2019 at 18:32
  • @skyboyer you are not blocking the server time, your application doesn't care about data that is not important for it and the code is simpler, it is easier to debug because you see an aborted request in the network tools and you are sure that your app is free from the race conditions Commented Dec 26, 2019 at 19:47
  • @MateuszKrzyżanowski, I strongly believe server will need some additional code to detect connection closed before sending response(say PHP). So by default we won't save server time. Sorry for offtop. Commented Dec 26, 2019 at 20:42

2 Answers 2

2

Hack for your particular case

Assuming you're receiving id as props or some such, would this work?

const Foo = ({ id }) => {
  const [data, setData] = useState()

  const passedId = useRef()
  passedId.current = id

  useInterval(() => {
    fetchData(id)
      .then(response => id === passedId.current && setData(response))
  }, 5000)

  // Render some JSX with the data

I tested something very similar to this locally and essentially what happens is this:

  1. Foo receives id = 6
  2. 6 is stored in passedId
  3. useInterval ticks and we request id = 6
  4. the component receives id = 7
  5. 7 is stored in passedId
  6. the request for id = 6 completes, but passedId.current === 7 so setData isn't called
  7. the request for id = 7 completes, id === passedId.current and setData is called

Getting something like this into useInterval

Note - not tested

Maybe the reason why cancelling an effect is so easy is because the function returns its own cleanup so ignore doesn't have to be scoped externally. But setInterval doesn't allow return values. We might be able to get around this using a setTimeout instead:

function useInterval(callback, delay) {
  useEffect(() => {
    let cleanup
    let id
    function tick() {
      cleanup = callback()
      id = setTimeout(tick, delay)
    }
    if (delay !== null) {
      tick()
      return () => {
        clearTimeout(id)
        cleanup && cleanup()
      }
    }
    return () => cleanup && cleanup()
  }, [callback, delay])

One catch with this is that now we have callback in the dependency array so the callback given to useInterval should be created using useCallback:

const Foo = ({ id }) => {
  const [data, setData] = useState()

  const pollData = useCallback(() => {
    let shouldUpdate = true

    fetchData(id).then(resp => shouldUpdate && setData(resp))

    return () => shouldUpdate = false
  }, [id])

  useInterval(pollData, 5000)

  // Render some JSX with the data
  1. When useInterval is called, we setup a useEffect that executes callback
  2. When callback executes, the return value (the cleanup for it) is stored in cleanup which is scoped to the effect and a setTimeout is setup to re-execute in delay milliseconds
  3. If callback changes (i.e. we get a new id), then we clear the timeout and run the cleanup for the previous callback
  4. After delay milliseconds, tick is executed again
Sign up to request clarification or add additional context in comments.

7 Comments

I have a little doubt. Assuming that the id has not been changed, after the first pollData request is sent, the second pollData request is sent immediately. At this time, both cleanup and id correspond to the second request. If both pollData requests are not resolved at this time and the id changes at this time, does it mean that only the second pollData request can be cleared, and the first pollData will still execute the setData function
@weiwang Sorry, I'm not sure I understand your example but the proposed code could very well be wrong! Could you help me understand why the second pollData request would be sent immediately? That being said ... I feel like the proposed code is wrong because I don't see how it would call tick the first time :-\ Unfortunately I've been out of the React game for a couple years so I'm not sure I can properly test/edit this. Maybe I should just remove the proposal?
I added a call to tick() in the useEffect. That seems to make slightly more sense to me but I'm happy to get more feedback!
Hi, Mark. Thank you very much for your reply. I am a newcomer to react hook, so my opinion may not be correct. My concern is that if two fetchData requests have already been sent at this time, but the two fetchData requests are very slow, and the data has not yet been obtained. At this time, if the id changes later, the cleanup logic will be executed, but I think the value of shouldUpdate of the second fetchData request will be changed to false, but the value of shouldUpdate of the first fetchData will not be changed to false. I don't know if I am right, want to discuss with you.
@weiwang Ah well I think the thing here is that fetchData called in a useEffect and the return value of the useEffect is a function which calls cleanup. The return value of an effect runs before the next effect starts. So, hopefully, before a second fetchData request was made, the first cleanup function would be run and shouldUpdate would be set to false for the first request.
|
1

Here's the useInterval I came up with

function (callback, options, cleanup) {
  const savedCallback = useRef();

  // Remember the latest callback.
  useEffect(() => {
    savedCallback.current = callback;
  }, [callback]);

  // Set up the interval.
  useEffect(() => {
    function tick() {
      savedCallback.current();
    }
    if (options.delay !== null) {
      let id = setInterval(tick, options.delay);
      return () => {        
        clearInterval(id);
        cleanup && cleanup();
      }
    }
    return () => cleanup && cleanup();
  }, [options]);
}

And I use it so

const [fetchOptions, setFetchOptions] = useState({delay: 5000, id: 'someid'});

let ignore = false;
useInterval(
  () => {
    fetchData(fetchOptions.id).then(data => {
      if (!ignore) {
        setData(data);
      }
    });
  },
  fetchOptions,
  () => (ignore = true),
);

I'm not sure if there's a better way to write this. My concern is with the ignore variable being scoped to the context of the function/component. In the code example of the question, the ignore variable is inside the useEffect and feels cleaner.

The downside of this approach is that fetchOptions needs to be a useState variable otherwise it would reset the useInterval hook on every render if it was just a constant in the function.

3 Comments

Just a quick question - it looks like delay changed from being a positional argument to being a named parameter in an object. Was it the intent that ignore would be inside fetchOptions? If not, can useCallback handle creating a new fetcher when the id changes and then delay and cleanup remain positional?
Oh, is fetchOptions intended to start a new interval when delay OR id changes?
Yes, that was the intention, start a new interval when delay or id changes.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.