Scheduling a job in React Hooks inside useEffect

Viewed 585

I'm writing a kibana plugin and I'm facing some trouble with a scheduled function: I want to refresh my page (and getting information from the server) every 5 seconds and to do that I wrote a setInterval inside the useEffect that calls my refresh function.

Now, if I call the refresh function manually (with a button), it works fine, the states are valued correctly and so on. But if I call the refresh function inside the setInterval inside the useEffect, it's like the state are never setted. The hooks set the various state but after 5 seconds, to the next refresh call, the states are empty.

Code, very simple:

useEffect(()=>{
  refresh();
  setInterval(refresh, 5 * 1000); //called from here, the states are empty, called manually works fine 
                                  // and the states are filled correctly
}, []}

const refresh = () => {
//some code with various states update
}
1 Answers

The refresh function is created every render, but the useEffect runs only on first mount, so when the interval runs, refresh is in a stale closure - any state referenced inside refresh will refer to initial state only.

One option would be to have the interval call a function in a ref instead, a ref which gets set when the state changes and has a reference to the state currently being rendered:

const refreshRef = useRef();
refreshRef.current = useCallback(() => {
  //some code with various states update
}, [valueA, valueB]);
// Put all stateful values referenced in the callback into the dependency array

// If you want to clear the interval when the component unmounts:
const [intervalId, setIntervalId] = useState();
useEffect(()=>{
  refreshRef.current()
  setIntervalId(
    setInterval(() => refreshRef.current(), 5 * 1000)
  );
  return () => {
    clearInterval(intervalId);
  }
}, []);

Example working snippet:

const { useState, useRef, useCallback, useEffect } = React;

const App = () => {
  const [count, setCount] = useState(0);
  const refreshRef = useRef();
  refreshRef.current = useCallback(() => {
    //some code with various states update
    console.log(count);
  }, [count]);
  // Put all stateful values referenced in the callback into the dependency array

  // If you want to clear the interval when the component unmounts:
  const [intervalId, setIntervalId] = useState();
  useEffect(()=>{
    refreshRef.current()
    setIntervalId(
      setInterval(() => refreshRef.current(), 5 * 1000)
    );
    return () => {
      clearInterval(intervalId);
    }
  }, []);
  return (
    <div>
      {count}
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
};

ReactDOM.render(<App />, document.querySelector('.react'));
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<div class='react'></div>

It looks like you can't do

const refresh = useCallback(...

and then call refresh inside a useEffect which depends on refresh because then the timing of when the interval function would be disrupted whenever the state changes; in the example below, rather than the count being logged every 5 seconds, it's logged every 5 seconds and every time state changes. If you wanted to make sure the log occurred on mount, then 5 seconds after mount, then 10 seconds after mount, etc, regardless of state updates, you'd have to do some ugly messing around with timestamps - unless you used the useRef approach in my first snippet.

const { useState, useCallback, useEffect } = React;

const App = () => {
  const [count, setCount] = useState(0);
  const refresh = useCallback(() => {
    console.log(count);
  }, [count]);
  // Put all stateful values referenced in the callback into the dependency array

  // If you want to clear the interval when the component unmounts:
  const [intervalId, setIntervalId] = useState();
  useEffect(()=>{
    refresh();
    // Clear the existing interval, if another one is pending:
    clearInterval(intervalId);
    setIntervalId(
      setInterval(refresh, 5 * 1000)
    );
    return () => {
      clearInterval(intervalId);
    }
  }, [refresh]);
  return (
    <div>
      {count}
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
};

ReactDOM.render(<App />, document.querySelector('.react'));
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<div class='react'></div>

Or, without a dependency array in the useEffect, the refresh function it references would be in a stale closure on subsequent renders:

const { useState, useCallback, useEffect } = React;

const App = () => {
  const [count, setCount] = useState(0);
  const refresh = useCallback(() => {
    console.log(count);
  }, [count]);
  // Put all stateful values referenced in the callback into the dependency array

  // If you want to clear the interval when the component unmounts:
  const [intervalId, setIntervalId] = useState();
  useEffect(()=>{
    refresh();
    // Clear the existing interval, if another one is pending:
    clearInterval(intervalId);
    setIntervalId(
      setInterval(refresh, 5 * 1000)
    );
    return () => {
      clearInterval(intervalId);
    }
  }, []);
  return (
    <div>
      {count}
      <button onClick={() => setCount(count + 1)}>Increment</button>
    </div>
  );
};

ReactDOM.render(<App />, document.querySelector('.react'));
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
<div class='react'></div>

Related