State not updating when using React state hook within setInterval
Asked Answered
P

15

270

I'm trying out the new React Hooks and have a Clock component with a time value which is supposed to increase every second. However, the value does not increase beyond one.

function Clock() {
  const [time, setTime] = React.useState(0);
  React.useEffect(() => {
    const timer = window.setInterval(() => {
      setTime(time + 1);
    }, 1000);
    return () => {
      window.clearInterval(timer);
    };
  }, []);

  return (
    <div>Seconds: {time}</div>
  );
}

ReactDOM.render(<Clock />, document.querySelector('#app'));
<script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>

<div id="app"></div>
Potion answered 27/10, 2018 at 17:25 Comment(1)
There are great explanations to why this is happening. In case someone wants to also get the value https://mcmap.net/q/108577/-state-not-updating-when-using-react-state-hook-within-setinterval is a highly underrated hack around it.Metic
P
321

The reason is because the callback passed into setInterval's closure only accesses the time variable in the first render, it doesn't have access to the new time value in the subsequent render because the useEffect() is not invoked the second time.

time always has the value of 0 within the setInterval callback.

Like the setState you are familiar with, state hooks have two forms: one where it takes in the updated state, and the callback form which the current state is passed in. You should use the second form and read the latest state value within the setState callback to ensure that you have the latest state value before incrementing it.

Bonus: Alternative Approaches

Dan Abramov goes in-depth into the topic about using setInterval with hooks in his blog post and provides alternative ways around this issue. Highly recommend reading it!

function Clock() {
  const [time, setTime] = React.useState(0);
  React.useEffect(() => {
    const timer = window.setInterval(() => {
      setTime(prevTime => prevTime + 1); // <-- Change this line!
    }, 1000);
    return () => {
      window.clearInterval(timer);
    };
  }, []);

  return (
    <div>Seconds: {time}</div>
  );
}

ReactDOM.render(<Clock />, document.querySelector('#app'));
<script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>

<div id="app"></div>
Potion answered 27/10, 2018 at 17:25 Comment(8)
@YangshunTay If I just wanna read state value within setInterval, how should I do?Midshipman
@Midshipman Have you read Dan's post? overreacted.io/making-setinterval-declarative-with-react-hooks. If you just want to read it, you can read the updated value as part of the rendering at the bottom. If you want to trigger side effects, you can add a useEffect() hook and add that state to the dependency array.Potion
How would it look like if you would like to output the current state periodically with console.log in the setInterval function?Obduce
I want to read the time (in setInterval) and update if greater than some time. How to accomplish this?Recover
@Midshipman " If you just want to read it, you can read the updated value as part of the rendering at the bottom." Didn't get it can you kindly elaborate it a bitRecover
That blog post is great. I would love to see it translated to a more generic example, though. Explaining how to do it for any callback would be nice to double-check my work against.Chita
With empty brackets, UseEffect is supposed to run only once on mount, because it has no dependency on any resource, so how is it that it re-runs every second?Embosom
@Embosom where was it mentioned that the useEffect callback re-runs every second?Potion
F
60

As others have pointed out, the problem is that useState is only called once (as deps = []) to set up the interval:

React.useEffect(() => {
    const timer = window.setInterval(() => {
        setTime(time + 1);
    }, 1000);

    return () => window.clearInterval(timer);
}, []);

Then, every time setInterval ticks, it will actually call setTime(time + 1), but time will always hold the value it had initially when the setInterval callback (closure) was defined.

You can use the alternative form of useState's setter and provide a callback rather than the actual value you want to set (just like with setState):

setTime(prevTime => prevTime + 1);

But I would encourage you to create your own useInterval hook so that you can DRY and simplify your code by using setInterval declaratively, as Dan Abramov suggests here in Making setInterval Declarative with React Hooks:

function useInterval(callback, delay) {
  const intervalRef = React.useRef();
  const callbackRef = React.useRef(callback);

  // Remember the latest callback:
  //
  // Without this, if you change the callback, when setInterval ticks again, it
  // will still call your old callback.
  //
  // If you add `callback` to useEffect's deps, it will work fine but the
  // interval will be reset.

  React.useEffect(() => {
    callbackRef.current = callback;
  }, [callback]);

  // Set up the interval:

  React.useEffect(() => {
    if (typeof delay === 'number') {
      intervalRef.current = window.setInterval(() => callbackRef.current(), delay);

      // Clear interval if the components is unmounted or the delay changes:
      return () => window.clearInterval(intervalRef.current);
    }
  }, [delay]);
  
  // Returns a ref to the interval ID in case you want to clear it manually:
  return intervalRef;
}


const Clock = () => {
  const [time, setTime] = React.useState(0);
  const [isPaused, setPaused] = React.useState(false);
        
  const intervalRef = useInterval(() => {
    if (time < 10) {
      setTime(time + 1);
    } else {
      window.clearInterval(intervalRef.current);
    }
  }, isPaused ? null : 1000);

  return (<React.Fragment>
    <button onClick={ () => setPaused(prevIsPaused => !prevIsPaused) } disabled={ time === 10 }>
        { isPaused ? 'RESUME ⏳' : 'PAUSE 🚧' }
    </button>

    <p>{ time.toString().padStart(2, '0') }/10 sec.</p>
    <p>setInterval { time === 10 ? 'stopped.' : 'running...' }</p>
  </React.Fragment>);
}

ReactDOM.render(<Clock />, document.querySelector('#app'));
body,
button {
  font-family: monospace;
}

body, p {
  margin: 0;
}

p + p {
  margin-top: 8px;
}

#app {
  display: flex;
  flex-direction: column;
  align-items: center;
  min-height: 100vh;
}

button {
  margin: 32px 0;
  padding: 8px;
  border: 2px solid black;
  background: transparent;
  cursor: pointer;
  border-radius: 2px;
}
<script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>

<div id="app"></div>

Apart from producing simpler and cleaner code, this allows you to pause (and clear) the interval automatically by simply passing delay = null and also returns the interval ID, in case you want to cancel it yourself manually (that's not covered in Dan's posts).

Actually, this could also be improved so that it doesn't restart the delay when unpaused, but I guess for most uses cases this is good enough.

If you are looking for a similar answer for setTimeout rather than setInterval, check this out: https://mcmap.net/q/110735/-react-hooks-right-way-to-clear-timeouts-and-intervals.

You can also find declarative version of setTimeout and setInterval, useTimeout and useInterval, a few additional hooks written in TypeScript in https://www.npmjs.com/package/@swyg/corre.

Frug answered 10/12, 2019 at 19:20 Comment(0)
S
45

useEffect function is evaluated only once on component mount when empty input list is provided.

An alternative to setInterval is to set new interval with setTimeout each time the state is updated:

  const [time, setTime] = React.useState(0);
  React.useEffect(() => {
    const timer = setTimeout(() => {
      setTime(time + 1);
    }, 1000);
    return () => {
      clearTimeout(timer);
    };
  }, [time]);

The performance impact of setTimeout is insignificant and can be generally ignored. Unless the component is time-sensitive to the point where newly set timeouts cause undesirable effects, both setInterval and setTimeout approaches are acceptable.

Stefanistefania answered 28/1, 2019 at 7:55 Comment(0)
O
23

useRef can solve this problem, here is a similar component which increase the counter in every 1000ms

import { useState, useEffect, useRef } from "react";

export default function App() {
  const initalState = 0;
  const [count, setCount] = useState(initalState);
  const counterRef = useRef(initalState);

  useEffect(() => {
    counterRef.current = count;
  })

  useEffect(() => {
    setInterval(() => {
      setCount(counterRef.current + 1);
    }, 1000);
  }, []);

  return (
    <div className="App">
      <h1>The current count is:</h1>
      <h2>{count}</h2>
    </div>
  );
}

and i think this article will help you about using interval for react hooks

Ocana answered 16/4, 2021 at 21:53 Comment(0)
S
10

An alternative solution would be to use useReducer, as it will always be passed the current state.

function Clock() {
  const [time, dispatch] = React.useReducer((state = 0, action) => {
    if (action.type === 'add') return state + 1
    return state
  });
  React.useEffect(() => {
    const timer = window.setInterval(() => {
      dispatch({ type: 'add' });
    }, 1000);
    return () => {
      window.clearInterval(timer);
    };
  }, []);

  return (
    <div>Seconds: {time}</div>
  );
}

ReactDOM.render(<Clock />, document.querySelector('#app'));
<script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>

<div id="app"></div>
Shrubbery answered 28/1, 2019 at 8:22 Comment(2)
Why useEffect here is being called multiple times to update the time, while the dependencies array is empty, which means that the useEffect should be called only the first time the component/app renders?Guillermo
@Guillermo The function inside useEffect is called only once, when the component first renders indeed. But inside of it, there is a setInterval which is in charge of changing the time on a regular basis. I suggest you read a bit about setInterval, things should be clearer after that ! developer.mozilla.org/en-US/docs/Web/API/…Shrubbery
S
5
const [seconds, setSeconds] = useState(0);
  useEffect(() => {
    const interval = setInterval(() => {
      setSeconds((seconds) => {
        if (seconds === 5) {
          setSeconds(0);
          return clearInterval(interval);
        }
        return (seconds += 1);
      });
    }, 1000);
  }, []);

Note: This will help to update and reset the counter with useState hook. seconds will stop after 5 seconds. Because first change setSecond value then stop timer with updated seconds within setInterval. as useEffect run once.

Spectroscope answered 18/2, 2022 at 8:16 Comment(1)
It helped a lot. Question asked in all Interviews.Athapaskan
Z
1

This solutions dont work for me because i need to get the variable and do some stuff not just update it.

I get a workaround to get the updated value of the hook with a promise

Eg:

async function getCurrentHookValue(setHookFunction) {
  return new Promise((resolve) => {
    setHookFunction(prev => {
      resolve(prev)
      return prev;
    })
  })
}

With this i can get the value inside the setInterval function like this

let dateFrom = await getCurrentHackValue(setSelectedDateFrom);
Zanthoxylum answered 27/8, 2019 at 16:56 Comment(1)
That's a bad practice, React state setter should be pure, no side-effects. Also, calling some setter just to get the current value would still trigger a re-render of the current component.Moquette
B
1
function Clock() {
  const [time, setTime] = React.useState(0);
  React.useEffect(() => {
    const timer = window.setInterval(() => {
      setTime(time => time + 1);// **set callback function here** 
    }, 1000);
    return () => {
      window.clearInterval(timer);
    };
  }, []);

  return (
    <div>Seconds: {time}</div>
  );
}

ReactDOM.render(<Clock />, document.querySelector('#app'));
Beforehand answered 20/12, 2021 at 6:28 Comment(1)
Your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center.Committeewoman
C
1

Somehow similar issue, but when working with a state value which is an Object and is not updating.

I had some issue with that so I hope this may help someone. We need to pass the older object merged with the new one

const [data, setData] = useState({key1: "val", key2: "val"});
useEffect(() => {
  setData(...data, {key2: "new val", newKey: "another new"}); // --> Pass old object
}, []);
Churchwell answered 13/3, 2022 at 12:56 Comment(0)
C
0

Do as below it works fine.

const [count , setCount] = useState(0);

async function increment(count,value) {
    await setCount(count => count + 1);
  }

//call increment function
increment(count);
Congo answered 4/2, 2020 at 6:12 Comment(2)
Where is setInterval used in your answer ?Telex
Params of increment are also useless here.Moquette
A
0

I copied the code from this blog. All credits to the owner. https://overreacted.io/making-setinterval-declarative-with-react-hooks/

The only thing is that I adapted this React code to React Native code so if you are a react native coder just copy this and adapt it to what you want. Is very easy to adapt it!

import React, {useState, useEffect, useRef} from "react";
import {Text} from 'react-native';

function Counter() {

    function useInterval(callback, delay) {
        const savedCallback = useRef();
      
        // Remember the latest function.
        useEffect(() => {
          savedCallback.current = callback;
        }, [callback]);
      
        // Set up the interval.
        useEffect(() => {
          function tick() {
            savedCallback.current();
          }
          if (delay !== null) {
            let id = setInterval(tick, delay);
            return () => clearInterval(id);
          }
        }, [delay]);
      }

    const [count, setCount] = useState(0);

  useInterval(() => {
    // Your custom logic here
    setCount(count + 1);
  }, 1000);
  return <Text>{count}</Text>;
}

export default Counter;
Antiphonal answered 12/10, 2021 at 3:10 Comment(0)
R
0
  const [loop, setLoop] = useState(0);
  
  useEffect(() => {
    setInterval(() => setLoop(Math.random()), 5000);
  }, []);

  useEffect(() => {
    // DO SOMETHING...
  }, [loop])
Replacement answered 11/12, 2021 at 21:47 Comment(1)
Welcome to StackOverflow. While your answer may solve the problem, it lacks an explanation about the code you have posted. Please check out the blogs on answering questions for more information.Manaus
M
0

For those looking for a minimalist solution for:

  1. Stop interval after N seconds, and
  2. Be able to reset it multiple times again on button click.

(I am not a React expert by any means my coworker asked to help out, I wrote this up and thought someone else might find it useful.)


  const [disabled, setDisabled] = useState(true)
  const [inter, setInter] = useState(null)
  const [seconds, setSeconds] = useState(0)

  const startCounting = () => {
    setSeconds(0)
    setDisabled(true)
    setInter(window.setInterval(() => {
        setSeconds(seconds => seconds + 1)
    }, 1000))
  }

  useEffect(() => {
      startCounting()
  }, [])

  useEffect(() => {
    if (seconds >= 3) {
        setDisabled(false)
        clearInterval(inter)
    }
  }, [seconds])

  return (<button style = {{fontSize:'64px'}}
      onClick={startCounting}
      disabled = {disabled}>{seconds}</button>)
}
Misreport answered 7/2, 2022 at 17:0 Comment(0)
G
0
 const [toggle, setToggle] = useState(false);
 const myFunction = () => {
    // your code and your setState()
   // ....
  setToggle((prev) => !prev);
 };

useEffect(() => {
 const interval = setInterval(myFunction, 60 * 1000); // every one minute
return () => clearInterval(interval);
}, [toggle]);
Gabbey answered 17/10, 2023 at 12:50 Comment(0)
C
-1

Tell React re-render when time changed.opt out

function Clock() {
  const [time, setTime] = React.useState(0);
  React.useEffect(() => {
    const timer = window.setInterval(() => {
      setTime(time + 1);
    }, 1000);
    return () => {
      window.clearInterval(timer);
    };
  }, [time]);

  return (
    <div>Seconds: {time}</div>
  );
}

ReactDOM.render(<Clock />, document.querySelector('#app'));
<script src="https://unpkg.com/[email protected]/umd/react.development.js"></script>
<script src="https://unpkg.com/[email protected]/umd/react-dom.development.js"></script>

<div id="app"></div>
Cold answered 20/3, 2019 at 10:37 Comment(2)
The problem with this is that the timer will be cleared and reset after every count change.Cold
And because so setTimeout() is preferred as pointed out by EstusSeadon

© 2022 - 2024 — McMap. All rights reserved.