4

I am unable to access state in a React functional component from a setInterval or setTimeout callback.

A very simple example updating state in one interval and attempting to access state with bound function and unbound arrow interval callbacks.

https://codesandbox.io/s/pedantic-sinoussi-ycxin?file=/src/App.js

const IntervalExample = () => {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    const interval1 = setInterval(() => {
      setSeconds((seconds) => seconds + 1);
    }, 1000);

    const interval2 = setInterval(() => {
      // this retrieves only the initial state value of 'seconds'
      console.log("interval2: seconds elapsed", seconds);
    }, 1000);

    const interval3 = setInterval(function () {
      // this retrieves only the initial state value of 'seconds'
      console.log("interval3: seconds elapsed", seconds);
    }, 1000);

    return () => {
      clearInterval(interval1);
      clearInterval(interval2);
      clearInterval(interval3);
    };
  }, []);

  return (
    <div className="App">
      <header className="App-header">
        {seconds} seconds have elapsed since mounting.
      </header>
    </div>
  );
};

I would like to make this work in both function and arrow cases. Any help appreciated.

Thanks

Laurence Fass
  • 1,614
  • 3
  • 21
  • 43
  • 1
    Try creating a custom hook like `useInterval` as shown in demo of [this post](https://stackoverflow.com/a/66804905/2873538). This hook will be better than dealing with `setTimeout`. – Ajeet Shah Mar 25 '21 at 18:28

3 Answers3

2

You are seeing initial state values inside setInterval due to stale closure, you can use setSeconds (state setter function) as a workaround to fix the issue of stale closure:

const IntervalExample = () => {
  const [seconds, setSeconds] = useState(0);

  useEffect(() => {
    const interval1 = setInterval(() => {
      setSeconds((seconds) => seconds + 1);
    }, 1000);

    const interval2 = setInterval(() => {
      let s = 0;
      setSeconds((seconds) => {s = seconds; return seconds;}); // HERE
      console.log("interval2: seconds elapsed", s);
    }, 1000);

    const interval3 = setInterval(function () {
      let s = 0;
      setSeconds((seconds) => {s = seconds; return seconds;}); // HERE
      console.log("interval3: seconds elapsed", s);
    }, 1000);

    return () => {
      clearInterval(interval1);
      clearInterval(interval2);
      clearInterval(interval3);
    };
  }, []);

  return (
    <div className="App">
      <header className="App-header">
        {seconds} seconds have elapsed since mounting.
      </header>
    </div>
  );
};

You can also use ref variables if you don't need to show seconds at UI. Note that returning the same value i.e. seconds in setSeconds function would not cause a re-render.

Ajeet Shah
  • 18,551
  • 8
  • 57
  • 87
2

Ive used the useStateAndRef hook which works for both function and arrow callbacks.

import "./styles.css";
import React, { useState, useRef, useEffect } from "react";

function useStateAndRef(initial) {
  const [value, setValue] = useState(initial);
  const valueRef = useRef(value);
  valueRef.current = value;
  return [value, setValue, valueRef];
}

const IntervalExample = () => {
  const [seconds, setSeconds, refSeconds] = useStateAndRef(0);

  useEffect(() => {
    console.log("useEffect[seconds] executing");

    const interval1 = setInterval(() => {
      setSeconds((seconds) => seconds + 1);
    }, 1000);

    const interval2 = setInterval(() => {
      // this retrieves only the initial state value of 'seconds'
      console.log("interval2: seconds elapsed", refSeconds.current);
    }, 1000);

    const interval3 = setInterval(function () {
      // this retrieves only the initial state value of 'seconds'
      console.log("interval3: seconds elapsed", refSeconds.current);
    }, 1000);

    return () => {
      clearInterval(interval1);
      clearInterval(interval2);
      clearInterval(interval3);
    };
  }, [seconds]);

  return (
    <div className="App">
      <header className="App-header">
        {seconds} seconds have elapsed since mounting.
      </header>
    </div>
  );
};

export default function App() {
  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <h2>Start editing to see some magic happen!</h2>
      <IntervalExample />
    </div>
  );
}

Laurence Fass
  • 1,614
  • 3
  • 21
  • 43
-2

Updated sandbox without dependencies in useEffect

Alex Chirkin
  • 105
  • 2
  • Thanks. that appears to work but I think this is creating new intervals every time "seconds" updates. Ive modified to console.log the useffect method which looks like its going to consume a lot of system resources over time. https://codesandbox.io/s/pedantic-sinoussi-ycxin?file=/src/App.js – Laurence Fass Mar 23 '21 at 12:11
  • Alex please update with correction and i will update response. – Laurence Fass Mar 23 '21 at 12:18