Handling SyntheticEvent in Asynchronous Code
A common React pitfall is trying to use event properties after an asynchronous operation, when the SyntheticEvent has already been nullified by React for performance reasons.
The Problem
function handleClick(event) {
// ❌ Problematic - event will be nullified
setTimeout(() => {
console.log(event.target.value); // Error!
}, 1000);
}
React’s SyntheticEvent objects are pooled and reused, meaning they’re nullified after the event handler completes for performance optimization.
Solutions
1. Extract Values Immediately
function handleClick(event) {
// ✅ Correct - extract values synchronously
const value = event.target.value;
setTimeout(() => {
console.log(value); // Works
}, 1000);
}
2. Use event.persist()
function handleClick(event) {
// ✅ Correct - removes event from pool
event.persist();
setTimeout(() => {
console.log(event.target.value); // Works
}, 1000);
}
3. For Forms, Use Component State
function MyForm() {
const [inputValue, setInputValue] = useState('');
const handleChange = (event) => {
// ✅ Store value in state immediately
setInputValue(event.target.value);
};
const handleSubmit = async () => {
// ✅ Use state value in async code
await fetch('/api', { body: inputValue });
};
return (
<form>
<input value={inputValue} onChange={handleChange} />
<button onClick={handleSubmit}>Submit</button>
</form>
);
}
Best Practices
- Extract needed values from events synchronously
- Use state to store values needed later
- Avoid
event.persist()
unless absolutely necessary (has performance implications) - For forms, consider controlled components pattern:
const [formData, setFormData] = useState({
username: '',
password: ''
});
const handleChange = (e) => {
setFormData({
...formData,
[e.target.name]: e.target.value
});
};
Common Use Cases
1. With Async/Await
async function handleSubmit(event) {
event.preventDefault();
const formData = new FormData(event.target); // ✅ Extract immediately
try {
const response = await fetch('/api', {
method: 'POST',
body: formData
});
// Can't use event here
} catch (error) {
// Handle error
}
}
2. With Promises
function handleClick(event) {
const buttonText = event.target.textContent; // ✅ Extract immediately
somePromise()
.then(() => {
console.log(buttonText); // Works
})
.catch(() => {
// Handle error
});
}
Remember that React’s event system is optimized for performance, so always assume SyntheticEvent objects won’t be available after the synchronous execution of your event handler completes.