TL;DR: useState updates are async. If you call a function in the same tick as setState, that function reads the old value. Use a useRef alongside your state and the ref is available immediately.

The Setup

I'm building a cancer education business's finder tool — an AI chat widget that connects patients with cancer treatment centers.

One feature: deep links. An email campaign, a partner site, or a direct ad can land you at ?center=<takeshapeId> (TakeShape is the CMS powering the facility database) and the chat widget starts up already scoped to that center, no browsing required.

On first load, a useEffect fires, reads the ?center= param, sets the facility ID in state, and then immediately fires an opening chat message so the conversation starts automatically.

Simple. Obvious. Broken.

The Wall

I refactored the deep-link system to use TakeShape IDs directly instead of URL slugs — cleaner, faster, no DB lookup needed. (Commit 6c81f0a. Hour 15:01.)

Then I tested the deep-link flow.

The first chat message hit the server with facilityTakeshapeId: undefined.

Every time. The AI had no idea which center it was supposed to be talking about. It was just… talking into the void.

What I Got Wrong

My first instinct was that the slug→ID refactor broke something. I spent time staring at that commit.

It wasn't that. The bug was always there. The refactor just made it impossible to miss.

Here's why: slug-based deep-linking required an async fetch to resolve the slug to a facility ID. That network hop gave React time to re-render, so by the time handleSend fired, the state update had actually landed. Strip out the async fetch and suddenly setFacilityTakeshapeId and handleSend are running back-to-back in the same synchronous block — and the race is OBVIOUS.

The Real Bug

The useEffect looked roughly like this:

setFacilityTakeshapeId(center);   // schedules a state update
// ...
handleSend(openingMessage);       // reads facilityTakeshapeId from closure

setFacilityTakeshapeId doesn't update facilityTakeshapeId right now. It schedules a re-render. React batches that update and applies it on the next render cycle.

handleSend runs in the same tick, reads facilityTakeshapeId from its closure — which still holds the old value: undefined.

The Fix That Worked

Three lines. Commit d703db7.

// 1. Declare a ref alongside the state
const facilityTakeshapeIdRef = useRef<string | undefined>(undefined);

// 2. Set it synchronously in the useEffect, right after setState
setFacilityTakeshapeId(center);
facilityTakeshapeIdRef.current = center;  // <-- available NOW, same tick

// 3. Read the ref (with state as fallback) when building the request
body: JSON.stringify({
  ...,
  facilityTakeshapeId: facilityTakeshapeIdRef.current || facilityTakeshapeId,
})

Refs are just mutable objects — .current = center takes effect immediately, no render cycle required. By the time handleSend reads facilityTakeshapeIdRef.current, the value is already there.

Why This Matters to Me

This is the kind of bug that hides in plain sight for months — until you remove the accidental delay that was masking it. The async network call was acting as an invisible buffer, and I didn't know it.

The rule I'm carrying forward: if you need a value in the same synchronous block you're setting it, useState won't save you. Reach for a ref, set both together, read the ref. It's not a hack — it's exactly what refs are for.