State not updating when using React state hook within setInterval

I'm trying out the new React Hooks and have a Clock component with a counter 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>

Answers:

Answer

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>

Answer

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.

Answer

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>

Answer

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);
Answer

If someone need to manage a queue

Let's say for showing notifications with interval of 3 sec (first in, first out), with ability to push new messages at any time.

Codesandbox example.

import React, {useState, useRef, useEffect} from "react";
import ReactDOM from "react-dom";

import "./styles.css";

let x = 1 // for testing
const fadeTime = 3000 // 3 sec 

function App() {
  // our messages array in what we can push at any time
  const [queue, setQueue] = useState([]) 

  // our shiftTimer that will change every 3 sec if array have items
  const [shiftTimer, setShiftTimer] = useState(Date.now())

  // reference to timer
  const shiftTimerRef = useRef(null)

  // here we start timer if it was mot started yet
  useEffect(() => {
    if (shiftTimerRef.current === null && queue.length != 0) {
      startTimer()
    }
  }, [queue])

  // here we will shift first message out of array (as it was already seen)
  useEffect(() => {
    shiftTimerRef.current = null
    popupShift()
  }, [shiftTimer])

  function startTimer() {
    shiftTimerRef.current = setTimeout(() => {
      setShiftTimer(Date.now)
    }, fadeTime )
  }

  function startTimer() {
    shiftTimerRef.current = setTimeout(() => setShiftTimer(Date.now), fadeTime )
  }

  function popupPush(newPopup) {
    let newQueue = JSON.parse(JSON.stringify(queue))
    newQueue.push(newPopup)
    setQueue(newQueue)
  }

  function popupShift() {
    let newQueue = JSON.parse(JSON.stringify(queue))
    newQueue.shift()
    setQueue(newQueue)
  }

  return (
    <div>
      <button onClick={() => popupPush({ message: x++ })}>Push new message</button>
      <div>{JSON.stringify(queue)}</div>
    </div>
  )
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);
Answer

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);
Answer

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>

Answer

As others have pointed out, the problem is that useState is only called once (as its 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 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://stackoverflow.com/a/59274757/3723993.

You can also find declarative version of setTimeout and setInterval, useTimeout and useInterval, plus a custom useThrottledCallback hook written in TypeScript in https://gist.github.com/Danziger/336e75b6675223ad805a88c2dfdcfd4a.

Tags

Recent Questions

Top Questions

Home Tags Terms of Service Privacy Policy DMCA Contact Us Javascript

©2020 All rights reserved.