I'm experiencing an issue with a TextField where e.target.value
becomes stale after an async operation(FileReader API) in the onChange handler.
When I capture e.target.value in a variable before the async operation and
use that variable to update state, everything works fine. However, if I try to
access e.target.value after the async operation completes, it always contains
the old value instead of the current input value.
Why does e.target.value lose its current value after an async operation, and
why does storing it in a variable beforehand solve the problem?
React version 19.0.0
import React from 'react';
export default function BasicCard() {
const [a, setA] = React.useState("place");
return <input
value={a}
onChange={async (e) => {
const value = e.target.value; // Storing in variable - WORKS
await new Promise((resolve) => { resolve(true) }); // FileReader API
console.log(2, e.target.value); // This logs the OLD value, not current
// setA(e.target.value); // This DOESN'T work - uses stale value
setA(value); // This WORKS - uses captured value
}}
/>
}
export function App(props) {
return (
<div className='App'>
<BasicCard />
</div>
)
}
Expected: e.target.value should contain the current input value even after the async operation.
Actual: e.target.value reverts to the previous value after the async operation completes, causing the state update to fail.
Workaround: Capturing e.target.value in a const before the async operation works correctly.
Edit: Steps to reproduce:
paste the above code in https://playcode.io/mui or minimal reproducible example here https://stackblitz.com/edit/vitejs-vite-fbcc9afp?file=src%2FApp.tsx
PS: deferred input can be used for better UX, there are definitely better ways to handle this, but I am just curious why is this happening
e.target.value becomes stale after an async operation because React reuses its synthetic event objects, and the underlying DOM input’s value may have changed since the event was first fired.
When you store e.target.value in a local variable before the async operation, you “snapshot” the current value while the event and its target are still fresh.
From the docs (uses a very similar example):
The
SyntheticEventobjects are pooled. This means that theSyntheticEventobject will be reused and all properties will be nullified after the event handler has been called. For example, this won’t work:function handleChange(e) { // This won't work because the event object gets reused. setTimeout(() => { console.log(e.target.value); // Too late! }, 100); }
The docs say this is not true for Reactv17+, which is misleading. For React 17+, e.persist is now a noop & React stopped physically pooling events, BUT the lifecycle semantics remain the same.
That means the real "fix" would be to store the value prior to avoid referencing a possibly outdated DOM element value.
It's because React tries to keep the DOM up to date with your state.
In your code you have:
export default function BasicCard() {
const [a, setA] = React.useState("place");
return <input value={a}
onChange={async (e) => {
await new Promise((resolve) => { resolve(true) });
setA(e.target.value);
}}
/>
}
You can see how value={a} and also how your change event is async. What appears to happen internally when any keypress happens in the input is:
await is encountered without changing the value of aBasicCarda as its valueawaiting and continues execution where it calls setA with e.target.value which is the value after the re-renderThe observation regarding event pooling is generally speaking a valid one, however it doesn't actually affect this particular case.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With