![]()
When working with React state in asynchronous operations, you need to be careful about stale closures and race conditions. Here are several approaches to safely set state without using functional updates:
1. Using State Variables Directly (With Caution)
const [count, setCount] = useState(0);
async function incrementAsync() {
const response = await fetch('/api/increment');
const newValue = await response.json();
// This works if you're sure you have the latest state
setCount(newValue);
}
Warning: This approach can lead to race conditions if multiple updates happen in rapid succession.
2. Using Refs to Track Current State
const [count, setCount] = useState(0);
const countRef = useRef(count);
// Update ref whenever state changes
useEffect(() => {
countRef.current = count;
}, [count]);
async function incrementAsync() {
const response = await fetch('/api/increment');
const newValue = await response.json();
// Use ref to ensure we have latest value
setCount(countRef.current + newValue);
}
3. Storing Async Results in Variables
const [user, setUser] = useState(null);
async function fetchUser() {
// Store the async result first
const userData = await fetch('/api/user').then(res => res.json());
// Then set state with the final value
setUser(userData);
}
4. Using Async/Await with State Setters
const [data, setData] = useState([]);
async function loadData() {
try {
const result = await fetch('/api/data');
const jsonData = await result.json();
setData(jsonData); // Safe because we're not depending on previous state
} catch (error) {
console.error('Failed to load data:', error);
}
}
5. Canceling Pending Requests (Advanced)
const [data, setData] = useState([]);
useEffect(() => {
const controller = new AbortController();
async function fetchData() {
try {
const response = await fetch('/api/data', {
signal: controller.signal
});
setData(await response.json());
} catch (error) {
if (error.name !== 'AbortError') {
console.error('Fetch error:', error);
}
}
}
fetchData();
return () => controller.abort();
}, []);
When to Use Functional Updates
While you asked about solutions without functional updates, it’s worth noting they solve many of these problems elegantly:
const [count, setCount] = useState(0);
async function incrementAsync() {
const response = await fetch('/api/increment');
const incrementBy = await response.json();
// Functional update ensures you get latest state
setCount(prevCount => prevCount + incrementBy);
}
Functional updates are particularly useful when:
- State updates depend on previous state
- Multiple updates might happen in quick succession
- You need to avoid race conditions
