David John Thammineni
React18 Mar 20212 min read

React Query: Stop Putting Server Data in Redux

React Query: Stop Putting Server Data in Redux

For years the default was: fetch in a thunk, normalize into Redux, select it back out, and hand-roll loading flags. React Query's insight is that server state is a cache, not state — and caches want different tools.

The before and after

Before, per endpoint: an action triple (REQUEST/SUCCESS/FAILURE), a reducer slice, a thunk, a selector, and manual isLoading flags. After:

import { useQuery } from "react-query";

function Projects() {
  const { data, isLoading, error } = useQuery(
    ["projects"],
    () => fetch("/api/projects").then((r) => r.json())
  );

  if (isLoading) return <Skeleton />;
  if (error) return <ErrorState error={error} />;
  return <ProjectList projects={data} />;
}

Caching, deduplication (ten components can call this hook, one request fires), background refetching on window focus, and retry logic — all included.

Staleness is the core concept

useQuery(["projects"], fetchProjects, {
  staleTime: 60_000,   // fresh for 1 min: no refetch on remount
  cacheTime: 300_000,  // keep unused data 5 min before GC
});

staleTime is the knob people miss. Default is 0 — every remount refetches. For data that changes rarely, a generous staleTime eliminates request storms while keeping the instant-render-from-cache behavior.

Mutations with optimistic updates

const mutation = useMutation(updateProject, {
  onMutate: async (next) => {
    await queryClient.cancelQueries(["projects"]);
    const prev = queryClient.getQueryData(["projects"]);
    queryClient.setQueryData(["projects"], (old) =>
      old.map((p) => (p.id === next.id ? next : p))
    );
    return { prev };
  },
  onError: (_e, _next, ctx) =>
    queryClient.setQueryData(["projects"], ctx.prev),
  onSettled: () => queryClient.invalidateQueries(["projects"]),
});

Optimistic UI with rollback in fifteen lines.

Redux still earns its place for genuinely client-side state machines. But every usersById slice with a loading flag is React Query waiting to be adopted — we deleted about 40% of our Redux code and the app got more responsive, not less.

Keep Reading