OMAR
Field NotesCV
Stop Putting Server Data in useState
← All Notes
Frontend01 August 2026 · 3 min read

Stop Putting Server Data in useState

Most React state bugs are one mistake wearing different hats: treating a copy of the server's data as if you owned it. TanStack Query fixes the category, not the symptom.

There is a bug I have fixed in almost every React codebase I have been handed. It appears as a stale list, a double spinner, a form that saves and then shows the old value. Different symptoms, one cause: somebody put server data in useState.

Server state is not application state. It is a copy of data owned by another machine, which can change without telling you, which several components need at once, and which can fail to arrive. useState models none of that.

What you end up writing by hand

const [users, setUsers] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)

useEffect(() => {
  let cancelled = false
  fetch('/api/users')
    .then(r => r.json())
    .then(d => { if (!cancelled) setUsers(d) })
    .catch(e => { if (!cancelled) setError(e) })
    .finally(() => { if (!cancelled) setLoading(false) })
  return () => { cancelled = true }
}, [])

Twelve lines, and it still has no cache, no deduplication, no retry, no refetch when the tab regains focus, and no way for a second component to share the result. Every screen re-implements it slightly differently, which is why the bugs are never quite the same twice.

What it looks like with a query layer

const { data: users, isPending, error } = useQuery({
  queryKey: ['users'],
  queryFn: () => fetch('/api/users').then(r => r.json()),
})

The cancellation, caching, deduplication and retry are not written because they are not yours to write. Two components asking for ['users'] get one request. A third mounting a minute later gets the cached array immediately and a background refresh.

The part that changes how you design

Mutations invalidate rather than assign:

const mutation = useMutation({
  mutationFn: createUser,
  onSuccess: () => queryClient.invalidateQueries({ queryKey: ['users'] }),
})

You are not reaching into other components' state to patch it. You are saying this data is now questionable and letting anything that depends on it refetch. That single change removes a whole class of "why didn't the sidebar update" tickets, because no component is responsible for remembering who else cares.

Where it does not help

Genuinely local state — a modal's open flag, the current tab, a draft input value — belongs in useState or a store. Wrapping those in queries adds ceremony and buys nothing. The line is ownership: if a server can change it behind your back, it is server state.

The rule I hold teams to

Anything that arrived over the network goes through the query layer. No exceptions, because the exceptions are where the stale-data bugs live. It is one dependency and a mental model that takes an afternoon, and it deletes more code than it adds on any project past its first week.

Resources

ReactStateTanStack

Need this built properly?

I build secure, fast, bilingual platforms for clients across Egypt, Saudi Arabia, the UAE and Kuwait.

Keep Reading