DEV Community

KhaledSalem
KhaledSalem

Posted on

The question that actually decides your state stack (it's not 'Redux or React Query')

This is not a "Redux is dead" post. Redux is excellent at what it's for. So is React Query. So is Zustand.

The problem was never the tools. It was that I spent years comparing tools that answer completely different questions — and never noticed.

"Redux or React Query?" "Zustand or RTK?" These read like real choices. They're not. It's like asking "hammer or screwdriver?" before you know what you're building.

The decision that actually matters comes from one question almost nobody asks correctly:

Who owns the source of truth for each piece of state?

There isn't one kind of state. There are two.

Owned truth. The client is the source. Is the sidebar open, what's the theme, which step of the wizard are we on, what's selected right now. Nobody tells this state to go re-verify itself. It lives and dies by your decision.

Borrowed truth. The source lives somewhere else — the server. Whatever you hold on the client is a mirror of a truth that changes behind your back. Its entire job is staying close to reality: staleness, invalidation, deduping, refetch-on-focus, optimistic updates.

These are not two flavors of the same thing. The first is a storage problem. The second is a distributed-systems synchronization problem. They do not share a mental model.

The original sin

A decade of frontend pain came from one move: using one tool for both.

Here's borrowed truth managed as if you owned it:

// The anti-pattern: server state hand-managed in a reducer
const initialState = { users: [], loading: false, error: null };

function usersReducer(state, action) {
  switch (action.type) {
    case 'FETCH_USERS_START':   return { ...state, loading: true };
    case 'FETCH_USERS_SUCCESS': return { users: action.payload, loading: false, error: null };
    case 'FETCH_USERS_ERROR':   return { ...state, loading: false, error: action.error };
    default: return state;
  }
}
// Now hand-write the rest yourself:
// caching, dedup, staleness windows, refetch-on-focus, retries, pagination...
Enter fullscreen mode Exit fullscreen mode

Every line of that is you re-implementing React Query — badly, without the guarantees. The redux + sagas swamp people remember wasn't Redux being bad. It was borrowed truth shoved into a tool built for owned truth.

Split the two, and the swamp drains:

// Borrowed truth → React Query owns the hard part
const { data: users, isLoading } = useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
}); // caching, dedup, staleness, refetch, retries — handled

// Owned truth → Zustand owns the trivial part
const useUI = create((set) => ({
  sidebarOpen: false,
  toggleSidebar: () => set((s) => ({ sidebarOpen: !s.sidebarOpen })),
}));
Enter fullscreen mode Exit fullscreen mode

The famous combos are answers, not preferences

Once you split state by who owns the truth, the stacks stop being taste and start being derivations.

react-query + zustand — the separation bet

Most of your state turns out to be borrowed truth that was masquerading as client state. Once React Query absorbs it, what's left of your owned truth is small — so Zustand's near-zero-ceremony store is more than enough. React Query takes the hard distributed-systems problem; Zustand takes the trivial store. The modern default.

The one discipline it demands: never dump server data into Zustand. The boundary is the whole point.

rq + redux + sagas — the orchestration bet

This is the one people misread. Sagas are not a data-fetching tool. They're a concurrency-and-time model — CSP/coroutines for the frontend.

// Sagas model time & concurrency, not data
function* searchSaga() {
  yield takeLatest('SEARCH_INPUT', function* (action) {
    yield delay(300);                      // debounce
    const results = yield call(api.search, action.query); // auto-cancels the previous run
    yield put({ type: 'SEARCH_RESULTS', results });
  });
}
Enter fullscreen mode Exit fullscreen mode

You reach for this when client complexity isn't holding values — it's orchestrating processes over time: cancellable, concurrent, raced flows, websocket streams feeding a state machine, multi-step wizards with rollback. And it's redux specifically — not Zustand — because sagas need a central action stream to subscribe to. The Redux action log is the event bus the saga listens on.

Here's the catch: React Query already ate the data-fetching orchestration. What's left for sagas is pure client-side concurrency. If, after adopting React Query, your sagas have almost nothing to do — that's the signal you never needed them. Justified for trading terminals, collaborative editors, real-time control UIs. Not your average CRUD app. (And in 2026, XState is often a cleaner answer than sagas for this axis — an explicit state machine beats an implicit one.)

RTK only — the unification bet

The opposite philosophy: don't specialize, unify. RTK Query handles borrowed truth, slices handle owned truth, all in one store, one tree, one set of devtools.

Why that's a real architectural argument, not laziness: you get one serializable snapshot of the entire app — server cache + client state in a single object. That makes logout → wipe everything a one-liner, time-travel debugging span the whole app including the cache, SSR hydration of all of it, and offline persistence trivial. react-query + zustand can't hand you one clean snapshot of the universe, because they're two separate universes.

The bet: unification is worth more than React Query's edge-case ergonomics. Enterprise's friend.

The decision matrix

Stack Borrowed-truth ratio Client complexity One app snapshot? Cost Best fit
react-query + zustand high holding values no lowest small, senior teams
rq + redux + sagas high orchestrating time no highest concurrency-heavy domains
RTK only medium–high values + unification yes medium large enterprise teams

The function, in plain terms:

  • Mostly borrowed truth + client just holds values → react-query + zustand
  • Client orchestrates processes over time → rq + redux + sagas
  • Need one snapshot of the whole universe → RTK only

(Diagram: the staircase decision tree goes here — each question peels off one answer.)

The uncomfortable part for 2026

React Server Components and server actions are quietly pushing a large chunk of this state back to the server. The app whose state was half client-state is watching RSC eat that half. So the deepest question now sits one level above everything above:

Does this state need to live on the client at all?


Don't pick a state tool. Figure out who owns the truth and where it lives — the tool derives itself from the answer.

What's your current stack, and which of these three is it really? Drop it in the comments 👇

Top comments (0)