I haven’t found any similar implementations, so I’m wondering if this is a valid thing to do in React?
Problems with useState
-
It’s prone to stale value (which can happen in non-obvious ways). Example codepan. Note the
oddCounter
is 0, becausecounter
is stale. -
It can’t use async functions.
Workaround
This workaround uses only the setState
function.
// Mutex
class Mutex {
// Implementation omitted. These can be easily implemented with an instance field of Promise that's resolved in unlock.
lock = async () => {...};
unlock = () => {...};
}
// Component
const MyComponent = () => {
const [counter, setCounter] = useState(0);
const mutex = useRef(new Mutex());
const handler = useCallback(async () => {
await mutex.lock();
let curCounter = await new Promise((res, rej) => {
// Use setCounter as a "probe" to get the current counter.
setCounter((v) => {
res(v);
return v;
}
});
// Note: we can use async.
curCounter += await getDelta();
setCounter(curCounter);
mutex.unlock();
}, []); // Note dep list is empty, because this doesn't depend on counter
return (<Button onClick={handler}>Click</Button>);
}
This may seem contrived, but the logic can be abstracted into a reusabled high-order function with "setState" as input.
This solves both the stale variable (we don’t rely on "counter") and also allows using async.
There’re some caveats with this approach:
- Must use a mutex to prevent multiple function calls at the same time, otherwise
curCounter
might have race condition. This may incur a performance hit. - Might need to deal with re-entrant locks.
- Must remember to unlock, otherwise program will hang.
2
Answers
Option 1 –
oddCounter
is derived fromcounter
. Instead of using another state, recompute it on the fly. You can memoize it if the component re-renders due the changes other than thecounter
.Note:
+(counter % 2 === 1)
evaluates to 0 (false) or 1 (true).Option 2 – use a
useEffect
to react tocounter
changes, and updateoddCounter
:You can call
setOddCounter
within the call tosetCounter
, and thenincrement
doesn’t depend on either counter, so it never becomes stale.This doesn’t let you use async, but that only matters if the previous state is required for the async action.