← Go Back

Setting React Hooks states in a sync-like manner?

Broken Post?Let me know

Photo by Martino Pietropoli on Unsplash

Note 📝 to self...

When you have more than one states defined using useState and need to access updated state value sync-like manner...

I've asked a question in r/reactjs about emulating a callback of setState, which enables you to access updated state value.

Shawn "swyx" Wang posted React Hooks setState Gotcha, which addressed the same problem.

The problem is that useState is an async method just like setState so that when you try to access a state value as shown below,

function App() {
const [count, setCount] = useState(0);
const [message, setMessage] = useState("");
function increment() {
setCount(count + 1);
setMessage(`count is ${count}`);
}
function decrement() {
setCount(count - 1);
setMessage(`count is ${count}`);
}
return (
<div className="App">
...
</div>
);
}
view raw App-bad.js hosted with ❤ by GitHub

View this gist on GitHub

Buggy 🐛 one

Try it on CodeSandbox.

message value will always contain a number differ by 1 from count.

Count & message off by one

First Workaround

The first workaround was to use useEffect to update the message.

function App() {
const [count, setCount] = useState(0);
const [message, setMessage] = useState("");
function increment() {
setCount(count + 1);
}
function decrement() {
setCount(count - 1);
}
useEffect(() => setMessage(`count is ${count}`), [count, message]);
return (
,,,
);
}
view raw App-first attempt.js hosted with ❤ by GitHub

View this gist on GitHub

useEffect

But Dan "gaeron" Abramov has pointed out that

This is unnecessary. Why add extra work like running an effect when you already know the next value? Instead, the recommended solution is to either use one variable instead of two (since one can be calculated from the other one, it seems), or to calculate next value first and update them both using it together. Or, if you're ready to make the jump, useReducer helps avoid these pitfalls.

Dan "gaeron" Abramov

The gist is that, don't store calculated values.

2nd Workaround

The point of the Shawn & my problem was that we need to access the updated state value (kind of like in callback of setState).

So I ended up created my own hook (don't call it a "custom hook"), useAsyncState to mitigate the issue.

function useAsyncState(initialValue) {
const [value, setValue] = useState(initialValue);
const setter = x =>
new Promise(resolve => {
setValue(x);
resolve(x);
});
return [value, setter];
}
function App() {
// const [count, setCount] = useState(0);
// const [message, setMessage] = useState("");
const [count, setCount] = useAsyncState(0);
const [message, setMessage] = useAsyncState("");
function increment() {
setCount(count + 1).then(count => setMessage(`count is ${count}`));
}
function decrement() {
setCount(count - 1).then(count => setMessage(`count is ${count}`));
}
// OR use async/await...
async function increment() {
const newCount = await setCount(count + 1)
setMessage(`count is ${newCount}`);
}
async function decrement() {
const newCount = await setCount(count - 1)
setMessage(`count is ${newCount}`);
}
...
}
view raw useAsyncState.js hosted with ❤ by GitHub

View this gist on GitHub

useAsyncState

Try it on CodeSandbox.

I am using a promise, not accepting a callback as it makes code clunky possibly causing a callback hell.

And also with a setter promise, you can also use async/await syntax.