skip to Main Content

I have a component, StatusIndicator, that uses a Redux Toolkit query, q, to fetch a status value and display it.

I want to write a custom hook, usePolling, that can be configured using arguments to invoke q at intervals, updating StatusIndicator.

I have two problems:

  1. Even though I specify a dependency on isAuthenticated, the contents of the custom hook run repeatedly.
  2. How do I separate the state for each instance of the custom hook? I am using useState in an attempt to maintain the timeoutId for each instance. Is this correct?
export const StatusIndicator = () => {
  const isAuthenticated = useSelector((state) => state.auth.token);
  const { data, q } = useGetStatusQuery();

  usePolling({ callback: q }, [isAuthenticated]);

  return (<div>{{data.status}}</div>);
};

export const usePolling = ({ callback }, dependencies = []) => {
  const [instanceTimeoutId, setInstanceTimeoutId] = useState(null);

  // I only want this run when `isAuthenticated` changes
  useEffect(() => {
    (async function continuation() {
      clearTimeout(instanceTimeoutId);
      await callback();
      const { timeoutId, promise } = delay(interval);
      setInstanceTimeoutId(timeoutId);
      await promise;
      continuation();
    })();

    return () => clearTimeout(instanceTimeoutId);
  }, [...dependencies]);
};

2

Answers


  1. As I’ve mentioned in my comment, you’re using useState for the timeout handle, so every time the continuation function is called, it will call setInstanceTimeoutId, which will trigger a re-render of usePolling hook because of the state change.

    In fact, just a simple variable should be enough here. You could probably use useRef for the handle, but I think the simple let timeoutHandle should have no issues, since we’re only using the value in two places – within continuation and in the hook cleanup. Both will just access whatever the current value is at the time.

    export const usePolling = ({ callback }, dependencies = []) => {
      
      useEffect(() => {
        let timeoutHandle = null;
        (async function continuation() {
          clearTimeout(timeoutHandle);
          await callback();
          const { timeoutId, promise } = delay(interval);
          timeoutHandle = timeoutId;
          await promise;
          continuation();
        })();
        return () => clearTimeout(timeoutHandle);
      }, [...dependencies]);
    };
    
    Login or Signup to reply.
  2. I would also have used useReffor storing my timeoutId as pointed our @NickParsons comment.

    export const usePolling = ({ callback }, isAuthenticated, dependencies=[]) => {
      const instanceTimeoutId = useRef(null);
    
      useEffect(() => {
        return () => clearTimeout(instanceTimeoutId.current);
      }, [callback]);
    
      useEffect(() => {
        // Only start polling when isAuthenticated is true
        if (isAuthenticated) {
          (async function continuation() {
            clearTimeout(instanceTimeoutId.current);
            await callback();
            const { timeoutId, promise } = delay(interval);
            instanceTimeoutId.current = timeoutId;
            await promise;
            continuation();
          })();
        }
      }, [isAuthenticated, callback, ...dependencies]);
    };
    

    To solve the first issue inside the second useEffect I’ve checked the condition that if isAuthenticated is true; immediately invoke async function. This function clears any existing timeout, calls the callback function and after that set a new timeout using the delay function. Now, The timeout id is stored in instanceTimeoutId.current ref. After the delay is passed, the continuation function is called again.

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