EDIT (22 June 2020): as this question has some renewed interest, I realise there may be a few points of confusion. So I would like to highlight: the example in the question
Instead of trying to access the most recent state within a callback, use useEffect
. Setting your state with the function returned from setState
will not immediately update your value. The state updates are batched and updated
It may help if you think of useEffect()
like setState
's second parameter (from class based components).
If you want to do an operation with the most recent state, use useEffect()
which will be hit when the state changes:
const {
useState,
useEffect
} = React;
function App() {
const [count, setCount] = useState(0);
const decrement = () => setCount(count-1);
const increment = () => setCount(count+1);
useEffect(() => {
console.log("useEffect", count);
}, [count]);
console.log("render", count);
return (
<div className="App">
<p>{count}</p>
<button onClick={decrement}>-</button>
<button onClick={increment}>+</button>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render( < App / > , rootElement);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.6/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Update
You can create a hook for your setInterval
and call it like this:
const {
useState,
useEffect,
useRef
} = React;
function useInterval(callback, delay) {
const savedCallback = useRef();
// Remember the latest callback.
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]);
}
function Card(title) {
const [count, setCount] = useState(0);
const callbackFunction = () => {
console.log(count);
};
useInterval(callbackFunction, 3000);
useEffect(()=>{
console.log('Count has been updated!');
}, [count]);
return (<div>
Active count {count} <br/>
<button onClick={()=>setCount(count+1)}>Increment</button>
</div>);
}
const el = document.querySelector("#root");
ReactDOM.render(<Card title='Example Component'/>, el);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.6/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.6/umd/react-dom.production.min.js"></script>
<div id="root"></div>
Some further info on useEffect()
You can access the latest state
in setState
callback. But the intention is not clear, we never want to setState
in this case, it may confuse other people when they read your code. So you may want to wrap it in another hook that can express what you want better
function useExtendedState<T>(initialState: T) {
const [state, setState] = React.useState<T>(initialState);
const getLatestState = () => {
return new Promise<T>((resolve, reject) => {
setState((s) => {
resolve(s);
return s;
});
});
};
return [state, setState, getLatestState] as const;
}
const [counter, setCounter, getCounter] = useExtendedState(0);
...
getCounter().then((counter) => /* ... */)
// you can also use await in async callback
const counter = await getCounter();
For your scenario (where you cannot keep creating new callbacks and passing them to your 3rd party library), you can use useRef
to keep a mutable object with the current state. Like so:
function Card(title) {
const [count, setCount] = React.useState(0)
const [callbackSetup, setCallbackSetup] = React.useState(false)
const stateRef = useRef();
// make stateRef always have the current count
// your "fixed" callbacks can refer to this object whenever
// they need the current value. Note: the callbacks will not
// be reactive - they will not re-run the instant state changes,
// but they *will* see the current value whenever they do run
stateRef.current = count;
function setupConsoleCallback(callback) {
console.log("Setting up callback")
setInterval(callback, 3000)
}
function clickHandler() {
setCount(count+1);
if (!callbackSetup) {
setupConsoleCallback(() => {console.log(`Count is: ${stateRef.current}`)})
setCallbackSetup(true)
}
}
return (<div>
Active count {count} <br/>
<button onClick={clickHandler}>Increment</button>
</div>);
}
Your callback can refer to the mutable object to "read" the current state. It will capture the mutable object in its closure, and every render the mutable object will be updated with the current state value.
onClick={() => { clickHandler(); }}
This way you run the function as defined when you click it not when you declared the onClick handler.
React re-runs the hook function every time there is a change, and when it does so it re-defines your clickHandler()
function.
For the record, you could clean that up. Since you don't care what your arrow function returns you could write it as such.
onClick={e => clickHandler()}
I would use a combination of setInterval()
and useEffect()
.
setInterval()
on its own is problematic, as it might pop after the component has been unmounted. In your toy example this is not a problem, but in the real world it's likely that your callback will want to mutate your component's state, and then it would be a problem.useEffect()
on its own isn't enough to cause something to happen in some period of time.useRef()
is really for those rare occasions where you need to break React's functional model because you have to work with some functionality that doesn't fit (e.g. focusing an input or something), and I would avoid it for situations like this.Your example isn't doing anything very useful, and I'm not sure whether you care about how regular the timer pops are. So the simplest way of achieving roughly what you want using this technique is as follows:
import React from 'react';
const INTERVAL_FOR_TIMER_MS = 3000;
export function Card({ title }) {
const [count, setCount] = React.useState(0)
React.useEffect(
() => {
const intervalId = setInterval(
() => console.log(`Count is ${count}`),
INTERVAL_FOR_TIMER_MS,
);
return () => clearInterval(intervalId);
},
// you only want to restart the interval when count changes
[count],
);
function clickHandler() {
// I would also get in the habit of setting this way, which is safe
// if the increment is called multiple times in the same callback
setCount(num => num + 1);
}
return (
<div>
Active count {count} <br/>
<button onClick={clickHandler}>Increment</button>
</div>
);
}
The caveat is that if the timer pops, then you click a second later, then the next log will be 4 seconds after the previous log because the timer is reset when you click.
If you want to solve that problem, then the best thing will probably be to use Date.now()
to find the current time and use a new useState()
to store the next pop time you want, and use setTimeout()
instead of setInterval()
.
It's a bit more complicated as you have to store the next timer pop, but not too bad. Also that complexity can be abstracted by simply using a new function. So to sum up here's a safe "Reacty" way of starting a periodic timer using hooks.
import React from 'react';
const INTERVAL_FOR_TIMER_MS = 3000;
const useInterval = (func, period, deps) => {
const [nextPopTime, setNextPopTime] = React.useState(
Date.now() + period,
);
React.useEffect(() => {
const timerId = setTimeout(
() => {
func();
// setting nextPopTime will cause us to run the
// useEffect again and reschedule the timeout
setNextPopTime(popTime => popTime + period);
},
Math.max(nextPopTime - Date.now(), 0),
);
return () => clearTimeout(timerId);
}, [nextPopTime, ...deps]);
};
export function Card({ title }) {
const [count, setCount] = React.useState(0);
useInterval(
() => console.log(`Count is ${count}`),
INTERVAL_FOR_TIMER_MS,
[count],
);
return (
<div>
Active count {count} <br/>
<button onClick={() => setCount(num => num + 1)}>
Increment
</button>
</div>
);
}
And as long as you pass all the dependencies of the interval function in the deps
array (exactly like with useEffect()
), you can do whatever you like in the interval function (set state etc.) and be confident nothing will be out of date.
To solve exactly this issue I have created a react module for that. react-usestateref
(Reacrt useStateRef). E.g. of use:
var [state,setState,ref]=useStateRef(0)
It's works exaclty like useState
but in addition, it gives you the current state under ref.current
Learn more:
You can get the latest value by using the setState
For e.g.
var [state,setState]=useState(defaultValue)
useEffect(()=>{
var updatedState
setState(currentState=>{ // Do not change the state by get the updated state
updateState=currentState
return currentState
})
alert(updateState) // the current state.
})