Handling Synthetic Events with Async Operations in React
A common React pitfall occurs when trying to access event.target.value
after an asynchronous operation because React’s SyntheticEvent gets nullified for performance reasons.
The Problem: Nullified Event
function handleChange(event) {
// ❌ Problematic - event will be nullified by the time we use it
setTimeout(() => {
console.log(event.target.value); // Error: can't read property of null
}, 1000);
}
Why This Happens
- Event Pooling: React reuses SyntheticEvent objects for performance
- Nullification: Events are nullified after the handler completes
- Async Timing: By the time your async code runs, the event is gone
Solutions
1. Store Values Immediately
function handleChange(event) {
// ✅ Correct - store value immediately
const value = event.target.value;
setTimeout(() => {
console.log(value); // Works fine
}, 1000);
}
2. Use event.persist()
function handleChange(event) {
// ✅ Correct but less preferred - keeps event object
event.persist();
setTimeout(() => {
console.log(event.target.value); // Works
}, 1000);
}
Note: event.persist()
removes the event from the pool and may impact performance
3. Modern Approach (React 17+)
async function handleChange(event) {
// ✅ Modern approach - React 17+ doesn't pool events by default
const value = event.target.value;
await someAsyncOperation();
console.log(value); // Still accessible
}
React 17+ changed event pooling behavior, but it’s still good practice to extract values early
4. Using Refs for Form Values
function MyComponent() {
const inputRef = useRef();
const handleSubmit = async () => {
// ✅ Get current value directly from ref
console.log(inputRef.current.value);
};
return (
<input
ref={inputRef}
onChange={(e) => console.log(e.target.value)} // For immediate changes
/>
);
}
Best Practices
- Extract values early – Store needed properties before async operations
- Avoid
persist()
– Unless absolutely necessary - Use refs – For form elements when you need later access
- Consider state – Store values in component state if needed elsewhere
Class Component Example
class MyComponent extends React.Component {
handleChange = (event) => {
const value = event.target.value;
this.setState({ inputValue: value }, () => {
// Access via state in callback
console.log(this.state.inputValue);
});
};
render() {
return <input onChange={this.handleChange} />;
}
}
Common Scenarios
- Form submissions with API calls:
async function handleSubmit(event) {
event.preventDefault();
const formData = new FormData(event.target);
// Process sync data first
const email = formData.get('email');
// Then make async call
await submitForm(email);
}
- Debounced inputs:
const debouncedSearch = debounce((value) => {
console.log(value);
}, 300);
function handleChange(event) {
const value = event.target.value;
debouncedSearch(value);
}