Setting state inside an async function without using functional updates

Loading

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

Leave a Reply

Your email address will not be published. Required fields are marked *