Dec 12, 2025·8 min read

React Query or Redux for products full of server data

React Query or Redux works better when you split server cache from UI workflow. Learn a simple way to choose both without mixing jobs.

React Query or Redux for products full of server data

Why teams get stuck

Teams get stuck when they use one store for two different jobs. They put API data next to screen flow, form steps, filters, modals, and temporary edits, then expect one tool to keep all of it clean. It works for a while. Then the product grows, and every change starts touching something unrelated.

A product with lots of server data makes this worse. The app fetches lists, details, counts, permissions, drafts, and background updates. At the same time, the UI tracks things like "is the drawer open", "which step is active", or "did the user change this field but not save yet". Those are not the same kind of state, but teams often store them the same way.

That choice creates confusion fast. A developer updates a filter in the store, and three components rerender. Another developer resets a page after a save, and the app fires extra requests because the fetch logic also depends on that shared state. A small workflow change can wake up data fetching code that nobody meant to touch.

After a few rounds like that, people stop trusting where the real data lives. Is the latest customer record in the cache, in Redux, in local component state, or in some copied selector output? If the team cannot answer that in one sentence, bugs get harder to track.

Stale values are the part that hurts most. A screen can show data that looks fresh because it came from a global store a moment ago, but the server already changed. Someone edits a record, goes back to a list, and sees old numbers that still look valid. Then another feature reads that same stale copy and builds on top of it.

This is why debates like React Query or Redux often go in circles. The problem usually is not the tool itself. The problem is that one store ends up carrying both cache state and workflow state, and those two move at different speeds.

Once that happens, every new feature feels heavier than it should. Simple changes take longer, fetches multiply, rerenders spread, and the team starts coding around the state system instead of using it.

The two kinds of state

A lot of confusion starts when a team puts every moving part into one bucket and calls it "app state." That bucket usually holds two different things, and they age in different ways.

Server state comes from outside your app. An API sends it to you, and the data can become wrong a minute later because another user changed it, a background job finished, or the server recalculated something. A product list, invoice status, unread count, or customer profile all fit here.

This type of state needs caching, refetching, loading states, retries, and a clear rule for when stale data should refresh. If you skip those rules, people start looking at old data and stop trusting the screen.

Workflow state is local to the user's current task. It tracks what the person is doing right now: which modal is open, which step of checkout they are on, what filters they picked before hitting "Apply," or the draft text they have not saved yet. This state does not need freshness rules from the server. It needs predictable updates, easy resets, and a simple way to move through the UI.

That difference changes how you should manage it.

  • Server state changes on the server first, then your UI catches up.
  • Workflow state changes in the UI first, often on every click or keystroke.
  • Server state can expire and needs refetch logic.
  • Workflow state usually ends when the task ends.

Picture an order dashboard. The list of orders comes from the API, so it belongs in a React Query cache. The "bulk edit" drawer being open, the selected rows, and the unsaved note inside the form belong in Redux UI state or plain component state.

The React Query or Redux debate gets messy when teams ask one tool to do both jobs. React Query is good at keeping fetched data fresh. Redux is better for workflow state that many parts of the UI need to share. You can force either tool to cover the other job, but the code gets noisy fast. Teams end up writing reducers for cache problems or using query data to control modals and drafts.

Once you split server state vs workflow state, the code usually gets simpler. Bugs also get easier to explain, because you can ask one plain question: did the problem come from stale server data, or from the user's current UI flow?

What React Query should own

If the backend is the source of truth, keep that data in React Query. It fits anything you fetch, show, and refresh when the server changes.

That usually includes:

  • lists of records
  • single record pages
  • totals, badges, and counts
  • filtered or searched results

A product catalog is a simple example. The list page, the product detail page, the "12 low stock items" badge, and the results for "red shoes" all belong in the React Query cache because they all come from the API.

React Query should also manage the boring parts that teams often wire by hand. Loading states, error states, retries, background refresh, and refetching on focus are part of its job. When people copy that data into Redux, they usually end up rebuilding logic React Query already gives them.

Manual copies cause drift. A user edits one item, the detail page updates, but the list still shows old data because someone forgot to sync both places. With React Query, you can invalidate the right query after a change and let it fetch fresh data. That is simpler than keeping extra copies alive.

Cache settings matter here. Use stale time and cache time with intent instead of treating every screen the same. A sales dashboard may need fresh counts every minute, while a settings page can sit in cache much longer without causing trouble.

Search results also belong here, even when they feel temporary. If the result comes from the server, keep the query and its filters close to the fetch. That makes repeated searches faster and avoids a second store full of half-expired results.

Prefetching is one of the easiest wins. If users often open a detail page from a table, prefetch that record when they hover or when the row appears on screen. If they usually open the next tab after checkout, fetch that data early. Done well, this cuts the "blank screen, spinner, wait" feeling by a lot.

A simple test helps: if you can throw the data away and fetch it again from the server, React Query should probably own it. Let the cache hold server data, and let it do the refreshing work it was built for.

What Redux should own

The React Query or Redux debate gets simpler when you give each tool one job. Redux should hold the app's working memory: the choices a person makes while moving through a task, especially when those choices need to stay put across screens, drawers, and shared controls.

That usually means progress and intent, not fresh data from the server. A refetch can update an order record or a product list. It should not kick a user back to step 1, close a panel, or erase a draft they have not submitted.

A flow with several screens shows the split clearly. If someone edits a product across details, pricing, and publish, React Query can load the saved product. Redux can keep the active step, unsaved edits, and screen rules tied to that draft. The saved record and the user's in-progress work stay separate.

Good fits for Redux

  • Current step in a wizard or setup flow
  • Unsaved form edits before submit
  • Which tab, modal, drawer, or panel is open
  • Screen rules based on user actions, such as showing a warning after a billing change
  • Decisions that affect several components at once

That last case matters more than many teams expect. If one choice changes a header badge, enables a footer button, and opens a side panel, local component state gets messy fast. Redux gives those parts one shared place to read from.

Keep Redux small and deliberate. Do not copy full API responses into it just because many components need the same record. React Query already handles fetching, caching, refetching, and loading states better than Redux for server data.

A quick test helps: ask whether this state describes "what the server last said" or "what the user is doing right now." The first one belongs in the React Query cache. The second one usually belongs in Redux UI state.

How to split state step by step

Audit Your UI Flow
Find where forms, filters, and modals should stay separate from server truth.

Start with one feature, not the whole app. Pick something messy, like an order page with filters, a details panel, save actions, and a review modal.

Write down every piece of state that feature uses. Include the obvious data, but also the small stuff teams forget: selected tab, open modal, draft form values, pending step, sort order, last clicked item.

Then sort each item by one simple rule: where does the truth live? If the backend owns it and many users could change it, put it in the React Query cache. If it only exists to help one user move through a screen, keep it in Redux UI state or local component state.

  • Product list from the API, item details, permissions, and counts go to React Query.
  • Current wizard step, unsaved form edits, active filters, and whether a drawer is open stay in Redux or local state.
  • Temporary flags like "is user editing" or "show discard warning" also stay out of the cache.
  • If you have to refetch it after save, it usually belongs to server state.
  • If it should disappear on refresh, it usually belongs to workflow state.

After that, move fetch code first. Create React Query hooks for reads and mutations, and let them own loading, error, retries, and cache updates. This alone cuts a lot of Redux code.

Keep Redux slices small. A slice should describe a user flow, not mirror backend tables. "checkoutFlow" makes sense. "productsFromApi" usually does not.

Mutations are where teams mix jobs again. Do not stuff server responses into Redux just because a save button lives there. Let the mutation update or invalidate the React Query cache, then dispatch a clear UI action such as closing the modal, resetting the draft, or moving to the next step.

A clean split often looks like this: React Query loads the latest order, Redux tracks that the user is on step 3 with a warning banner open, and a successful mutation closes the banner and refreshes the order. React Query or Redux stops being an argument when each tool has one job.

A simple product example

Think about a B2B order screen. A sales rep picks a customer, checks stock, sees contract pricing, adds items, writes a note, and moves through approval. Everything sits on one page, but the page holds two very different kinds of state.

Customer details, stock counts, and prices come from the API. React Query should fetch those parts separately and keep each response in the React Query cache. That way, if stock changes for one item, the app can refresh that stock data without refetching the whole screen. The customer record stays put. The price data stays put unless it also changed.

The rep's in-progress work belongs somewhere else. Redux should hold the current step in the order flow, the selected items before submit, the draft note, and small UI choices like which panel is open. None of that is server truth yet. It is just the user's working state.

A clean split looks like this:

  • React Query stores customers, stock, prices, and saved orders
  • Redux stores the active step, selected lines, local filters, and draft notes
  • The form reads from both, depending on what it needs
  • Submit sends the Redux draft to the API
  • After success, React Query refreshes only the affected queries

Say the rep selects Customer A and adds 12 units of Item 42. React Query gives the latest stock and the right customer price. Redux keeps the chosen quantity and the note "hold for Friday delivery" while the rep edits the order.

When the rep clicks Submit, the app sends the draft order to the API. If the save works, React Query can invalidate just the queries that changed, such as the saved order list, the order detail, and the stock for Item 42. The app does not need to reload every customer, every price, or every screen.

This is where the React Query or Redux debate usually gets confused. On a screen full of server data, one tool should not do both jobs. React Query handles remote data that can go stale. Redux handles workflow state that exists because a person is in the middle of doing something.

Mistakes that cause churn

Fix Cache Boundaries
Get a second opinion on React Query, Redux, and where each piece should live.

Most React Query or Redux arguments start after a team mixes two different jobs in one place. The cache should hold server data. Redux should hold app flow, UI choices, and unsaved work. Once those lines blur, bugs feel random and cleanup takes longer than the feature did.

A common mistake is copying query data into Redux just so another screen can read it. That sounds harmless. It usually gives you two versions of the same customer, order, or project. React Query updates after a refetch, but the Redux copy stays old, and nobody can tell which one the UI should trust.

Putting form drafts into the React Query cache causes a different mess. A draft title, an unsaved note, or the current step in a checkout flow is not server data yet. If a background refetch runs while someone is typing, the cache can replace their draft with the last saved version. People call this a flaky form, but the storage choice caused it.

Teams also create churn when they refresh every related query after one tiny update. A user changes a status from "open" to "closed", and the app refetches lists, detail pages, counters, dashboards, and side panels. The UI flickers, the network gets noisy, and the code starts to feel fragile. Most of the time, a narrow invalidation or a small cache update is enough.

Another source of pain is hiding side effects inside random components. One button click might save data, clear a Redux slice, show a toast, close a modal, and redirect to another page, but those actions live in three different files. When something breaks, the team starts guessing. Keep those follow-up actions close to the mutation or in one small action layer that the whole team can find.

Naming drift sounds minor, but it wastes hours. If the query key says "customers" and Redux calls the same thing "accounts", people stop seeing the boundary between server state vs workflow state. The same happens when query keys use one ID shape and Redux slices use another. Shared names make bugs easier to spot.

A simple rule helps: if the server owns it, keep it in the React Query cache. If the user is still shaping it, keep it in Redux or local state. That split feels boring, and boring state management is usually the one that survives release week.

Quick checks before you add new state

Clean Up Mutations
Get help with cache updates, invalidations, and the UI actions after save.

Adding state too early creates most of the confusion. A team stores one value in React Query, copies it into Redux, then adds local component state on top. A week later, nobody trusts what they see on screen.

Pause for a minute and ask one plain question first: where does the truth live? That answer usually tells you whether React Query or Redux should own it.

Use these checks before you create a new slice, atom, or query:

  • If the server is the source of truth, keep that value in the React Query cache. Product price, stock count, unread messages, and account limits usually fit here.
  • If the server can change it without the user touching the page, do not copy it into Redux just to make it easier to read. Fetch it, cache it, and refetch when needed.
  • If the user edits a value before saving, treat it as draft state. Form fields, wizard progress, selected filters, and unsaved notes usually belong in component state or Redux UI state.
  • If the value should survive a route change or a full reload, decide that on purpose. Some state belongs in the URL, some in Redux, and some in local storage. Temporary input often does not need any of them.
  • If a mutation changes related data, name the refresh path right away. Know which query you will invalidate or update after save, delete, or approve.

A simple example helps. On an order screen, the order data, payment status, and inventory count come from the server. They can change behind the user's back, so React Query should own them. The open tab, unsaved refund note, and current step in a support workflow are workflow state. Redux can hold those if several parts of the UI need them.

If you cannot answer these checks in one sentence each, do not add new state yet. That pause saves real time later. It also stops the usual React Query or Redux argument, because the job of each tool gets much clearer.

What to do next

Start with one screen that causes daily friction. Pick the page where people filter, sort, edit, and revisit data all day. Do not plan a full rewrite. One busy screen is enough to show whether your split between React Query cache and Redux UI state makes the code calmer or more messy.

Make a quick map before you touch code. Put every piece of state into one of two buckets: data that comes from the server, and temporary app behavior such as open panels, draft filters, wizard steps, selected rows, or unsaved form progress. This small exercise often settles the React Query or Redux debate faster than another meeting.

A simple rule helps: if the server can refetch it, React Query should probably own it. If the user can lose it by refreshing the page and that is fine, Redux may own it. If a value only exists to help a person finish a task on the screen, keep it out of the query cache.

Write naming rules while the screen is still fresh in your mind:

  • Use query keys that match how people find data, such as product, product list, or order details.
  • Name Redux slices after user workflows, not API resources. Think checkout flow, bulk edit, or table controls.
  • Keep one short note on what never goes into Redux, such as fetched lists or record details.
  • Keep one short note on what never goes into React Query, such as modal visibility or stepper progress.

Then test the split with a real task. A product manager opens the orders page, changes filters, selects five rows, opens a side panel, and refreshes data. The list should stay in the query cache. The open panel, selected rows, and filter draft should stay in workflow state. If that feels awkward, your boundaries still need work.

If your team wants a second opinion, Oleg Sotnikov can review the app architecture and point out where state ownership got mixed. As a Fractional CTO, he helps teams set clean boundaries, cut waste, and avoid turning one state tool into two different jobs.

React Query or Redux for products full of server data | Oleg Sotnikov