I subscribe to Medium and get a daily email with a selection of javascript/programming articles. It's a good practice that helps me keep tabs on what's happening in the industry (or at least what other people think is happening in the industry).
Many times, though, I'm sent links to articles that are clearly written by beginner engineers. I'm glad to see that they are writing, but the content isn't something suited for me. As I read these articles, sometimes I forget how much I really do take for granted.
For example, today I read this article here. It's a good, quick read. But the section on stale state really stuck with me. I'm looking at the code thinking to myself, "Well of course this isn't going to work. Why would it?"
Specifically, I'm talking about the console.log inside of the setTimeout callback. It's actually a really good example to show how closures work (or don't) and the brain tease they can cause. The state
variable is enclosed in the closure at the time of definition, not execution. So, even though state is being incremented to your users' delight, the callback is using the value of state
when the app is loaded.
The author brought up an interesting problem, though.
What if I did want to setTimeout on a state value and have it in sync with my React UI?
So I threw the code into codepen and starting hacking away. Switching the state variable from useState
to useRef
solved the console.log problem.
Clicking on the button would increase the ref and the console.log would display the current and correct value.
However, this brought up a new issue. Updating ref's in React do not trigger a rerender. So, even though the value was being updated, the UI wasn't updating.
I poked around with useState and useCallback to augment the logic: my though process was create a separate state value that would trigger the update. I didn't quite make it, so decided to do a little Goggling.
Turns out, I wasn't too far away from an answer. I hadn't though about using useState and useCallback for non-rendering logic. For this unique and theoretical exercise it works quite well.
I moved the forceUpdate logic and the useRef logic into their own custom hooks so App would be easier to read. I also changed the increase function from a static increase by 1 to a function that accepts a value of how much to increase.
import { useState, useCallback, useEffect, useRef } from 'react';
function useForceUpdate() {
const [, updateState] = useState();
const forceUpdate = useCallback(() => updateState({}), []);
return [forceUpdate];
}
function useRefState() {
const state = useRef(0);
const [forceUpdate] = useForceUpdate();
const toggle = useCallback(
(num = 1) => () => {
state.current = state.current + num;
forceUpdate();
},
[]
);
return [state, toggle];
}
function App() {
const [value, inc] = useRefState();
useEffect(() => {
setInterval(() => console.log(`state`, value.current), 3000);
}, []);
return (
<div>
<h2>{value.current}</h2>
<button onClick={inc(5)}>Increase</button>
</div>
);
}
ReactDOM.render(<App />, document.getElementById("app"));
And there you go.
Happy coding.