# The Current Landscape#
Next.js App Router changed how teams think about data fetching: React Server Components run on the server, Client Components run in the browser, and Next.js fetch adds its own caching and deduping on top. That creates a common problem: you render data on the server, then your client cache library fetches the same data again after hydration.
This post compares React Query vs SWR in Next.js App Router with a focus on real production concerns: caching behavior, SSR and RSC compatibility, mutations, optimistic updates, developer experience, and concrete patterns to avoid double fetching.
ℹ️ Note: This comparison assumes Next.js App Router with React Server Components enabled, and modern versions of both libraries. For deeper RSC fundamentals, read React Server Components guide. For Next.js caching nuances and tradeoffs, see Next.js caching strategies SSR ISR SWR.
# Quick Comparison Table#
| Criteria | React Query (TanStack Query) | SWR (Vercel) |
|---|---|---|
| Primary model | Query cache with explicit keys and invalidation | Stale-while-revalidate key cache with revalidation |
| Best at | Complex server state, dashboards, SaaS CRUD, optimistic updates | Simple read-heavy fetching, small apps, content widgets |
| Mutations | First-class useMutation, retries, invalidation pipelines | mutate and mutation helpers, more manual orchestration |
| Optimistic updates | Strong, well-documented patterns | Possible, but usually more custom logic |
| RSC compatibility | Use in Client Components, hydrate from server | Use in Client Components, provide fallback data |
| Avoid double fetching | Prefetch server side and dehydrate, or pass initialData | Pass fallback via provider, or avoid server fetch for same key |
| Devtools | Excellent | Minimal |
| Ecosystem | Huge, multi-framework, strong patterns | Lean, smaller surface area |
| Learning curve | Medium | Low |
| Bundle size impact | Higher | Lower |
# How Caching Actually Works in App Router#
The confusion in App Router is that there are multiple caches in play:
- 1Next.js
fetchcache on the server: caches per request or per route segment depending oncacheandnext.revalidate. - 2RSC render deduping: repeated
fetchcalls with the same input can be deduped during a render. - 3Client cache: React Query or SWR caches data in the browser across renders, navigations, and refocus events.
Double fetching happens when you use both server fetch and client fetch for the same resource without hydrating the client cache.
The double fetching scenario in one sentence#
You fetch on the server for the initial HTML, then your Client Component mounts and fetches again because its cache is empty.
This is not only wasted bandwidth. It can add 100 ms to 500 ms of extra latency on slow networks and cause UI flicker when data changes between server render and client revalidate. For performance fundamentals and profiling, see website performance optimization.
# Caching Models: React Query vs SWR#
React Query cache model#
React Query treats server state as a normalized cache indexed by query keys. You control freshness using staleTime, and you control refetch behavior on focus, reconnect, and mount. You also get explicit invalidation and refetching via queryClient.invalidateQueries.
Practical implications:
- If your dashboard has 20 widgets and they share queries, React Query gives you predictable deduping and invalidation.
- If you update one entity, you can invalidate related queries by partial keys and let React Query refetch only what is stale.
SWR cache model#
SWR implements stale-while-revalidate: return cached data immediately, then revalidate in the background. It is intentionally minimal: a key, a fetcher, and revalidation controls. It excels when you want quick, simple client caching with lightweight configuration.
Practical implications:
- Great for simple pages and small client widgets that can revalidate opportunistically.
- For complex invalidation graphs, you usually end up calling
mutateacross multiple keys manually or writing helper utilities.
🎯 Key Takeaway: React Query gives you stronger primitives for large query graphs and invalidation; SWR stays simpler but pushes more orchestration to your app once complexity grows.
# SSR and RSC Compatibility in Next.js App Router#
Neither React Query nor SWR should be used directly inside Server Components. App Router expects you to fetch on the server using fetch or server actions, then pass data to Client Components.
Recommended mental model#
- Server Components: fetch with Next.js
fetchand route segment caching. - Client Components: use React Query or SWR for interactivity, background refetch, pagination, mutations, and optimistic updates.
- Avoid duplicate fetch: hydrate client cache with server-fetched data.
React Query SSR hydration pattern for App Router#
The most reliable approach is: prefetch server-side, dehydrate, then hydrate in a Client Provider. Keep the code minimal and consistent.
// app/providers.tsx
"use client";
import { QueryClient, QueryClientProvider, HydrationBoundary } from "@tanstack/react-query";
import { useState } from "react";
export function Providers(props: { state: unknown; children: React.ReactNode }) {
const [client] = useState(() => new QueryClient());
return (
<QueryClientProvider client={client}>
<HydrationBoundary state={props.state}>{props.children}</HydrationBoundary>
</QueryClientProvider>
);
}// app/page.tsx (Server Component)
import { dehydrate, QueryClient } from "@tanstack/react-query";
import { Providers } from "./providers";
async function getUser() {
const res = await fetch("https://api.example.com/me", { cache: "no-store" });
if (!res.ok) throw new Error("Failed");
return res.json();
}
export default async function Page() {
const qc = new QueryClient();
await qc.prefetchQuery({ queryKey: ["me"], queryFn: getUser });
const state = dehydrate(qc);
return (
<Providers state={state}>
{/* Client component uses useQuery(["me"]) without refetching */}
</Providers>
);
}Key points:
- Your
queryFnshould callfetchwith the correct caching semantics. - Prefer
cache: "no-store"for user-specific data, or a revalidate strategy for public data. - Set
staleTimein the client query to avoid immediate refetch on mount.
SWR fallback pattern for App Router#
SWR provides fallback to seed the cache. The pattern is similar: fetch on the server, pass as fallback, then use useSWR in Client Components with the same key.
// app/swr-provider.tsx
"use client";
import { SWRConfig } from "swr";
export function SWRProvider(props: { fallback: Record<string, unknown>; children: React.ReactNode }) {
return <SWRConfig value={{ fallback: props.fallback }}>{props.children}</SWRConfig>;
}// app/page.tsx (Server Component)
import { SWRProvider } from "./swr-provider";
async function getUser() {
const res = await fetch("https://api.example.com/me", { cache: "no-store" });
if (!res.ok) throw new Error("Failed");
return res.json();
}
export default async function Page() {
const user = await getUser();
return (
<SWRProvider fallback={{ "/api/me": user }}>
{/* Client component uses useSWR("/api/me") without refetching */}
</SWRProvider>
);
}Key points:
- The SWR key must match exactly, including query string and base path conventions.
- If your client fetcher points to a different URL than the server fetch, you will still double fetch.
⚠️ Warning: The most common double-fetch bug is mismatched keys. A server fetch to
https://api.example.com/meand a client fetch to/api/meare different caches and will both execute unless you unify the source or seed both.
# Mutations and Optimistic Updates#
Mutations are where the difference becomes most visible in real apps.
React Query mutations#
React Query has a dedicated mutation API with a clear lifecycle:
onMutatefor optimistic updatesonErrorto rollbackonSuccessto invalidate or update queriesonSettledfor cleanup
This is ideal for CRUD-heavy SaaS apps where perceived speed matters. A common product metric: even a 100 ms to 300 ms perceived improvement during frequent actions can reduce churn in internal tools, because the UI feels responsive.
Example optimistic update pattern:
"use client";
import { useMutation, useQueryClient } from "@tanstack/react-query";
export function useUpdateProfile() {
const qc = useQueryClient();
return useMutation({
mutationFn: async (payload: { name: string }) => {
const res = await fetch("/api/profile", {
method: "PATCH",
headers: { "content-type": "application/json" },
body: JSON.stringify(payload),
});
if (!res.ok) throw new Error("Failed");
return res.json();
},
onMutate: async (payload) => {
await qc.cancelQueries({ queryKey: ["me"] });
const prev = qc.getQueryData(["me"]);
qc.setQueryData(["me"], (old: any) => ({ ...old, name: payload.name }));
return { prev };
},
onError: (_err, _payload, ctx) => {
qc.setQueryData(["me"], ctx?.prev);
},
onSettled: () => {
qc.invalidateQueries({ queryKey: ["me"] });
},
});
}SWR mutations#
SWR supports mutation via mutate and mutation helpers, and you can do optimistic updates. The difference is that you often write more glue code for rollback and for updating multiple keys.
A common approach is:
mutate(key, updater, false)to update locally without revalidate- run the request
- revalidate or rollback
This is fine for a few endpoints. It becomes error-prone when you have multiple dependent caches, list and detail views, and pagination.
# Developer Experience and Team Workflow#
React Query DX#
React Query tends to win when a codebase grows:
- Query key conventions scale well across teams.
- Devtools make cache state and refetch reasons obvious.
- Invalidation patterns are consistent, which reduces bugs during feature work.
The tradeoff is learning the mental model: staleTime vs cacheTime, invalidation semantics, and hydration boundaries.
SWR DX#
SWR tends to win for speed-to-ship:
- Very low setup.
- Minimal concepts.
- Easy to sprinkle into small Client Components.
The tradeoff is that you will build your own conventions for keys, revalidation policies, and mutation patterns. Without strong conventions, larger teams end up with inconsistent caching behavior across the app.
💡 Tip: If you choose SWR for a growing app, define a single key builder utility early. Use one place to compose keys for list, detail, and filtered variants. This prevents 80 percent of accidental duplicate keys and invalidations later.
# Avoiding Double Fetching: Proven Patterns#
Double fetching is rarely a library bug. It is almost always an architectural mismatch.
Pattern A: Server fetch for first paint, hydrate client cache#
Use this when:
- SEO or fast initial paint matters.
- The same data is used immediately in a Client Component.
- You want interactivity after hydration without refetching.
React Query: prefetchQuery plus dehydrate plus HydrationBoundary.
SWR: server fetch plus SWRConfig fallback.
Pattern B: Client-only fetch, no server fetch for that data#
Use this when:
- Data is user-specific and not needed for initial HTML.
- You want to avoid server load and keep the route static.
- You can show skeletons or placeholders.
This is common for dashboards that render a frame quickly and load widgets progressively.
Pattern C: Let Next.js cache handle public content, use client cache sparingly#
For content sites:
- Fetch content in Server Components using
next.revalidate. - Avoid client caching libraries unless you have interactive widgets that need client-side revalidation.
This keeps bundle size smaller and reduces hydration work. It also aligns with App Router’s strengths.
Debug checklist for duplicate requests#
- 1Confirm whether the first request is server-side and the second is client-side.
- 2Verify key equality: exact SWR key string or React Query queryKey array.
- 3Verify URL equality: same path and query string, same base URL assumptions.
- 4Set
staleTimein React Query to prevent immediate refetch after hydration. - 5In SWR, disable
revalidateOnMountwhen you truly want to trust fallback for a period.
# Recommendations by App Type#
Dashboards and internal tools#
Typical traits: lots of widgets, filters, pagination, frequent mutations, and data that changes often.
Recommendation:
- Prefer React Query for predictable query graphs, deduping, and invalidation.
- Use server rendering for layout and critical summary data only, then hydrate widgets or load client-only.
Why it matters:
- Dashboards often trigger dozens of requests. React Query’s caching prevents repeated refetches when users navigate between tabs or open detail drawers.
SaaS products with CRUD and real-time-ish UX#
Typical traits: lists and detail views, forms, optimistic updates, multi-step flows, and high UX expectations.
Recommendation:
- Prefer React Query for mutations and optimistic updates.
- Use
invalidateQueriesstrategically rather than global refetching. - Consider pairing with server actions for writes and React Query for client cache updates.
Why it matters:
- Optimistic updates reduce perceived latency and make the product feel faster. React Query provides safer rollback patterns.
Content sites and marketing pages#
Typical traits: mostly read-only content, SEO-driven, relatively stable data, minimal client interactivity.
Recommendation:
- Prefer SWR only for small client widgets such as a newsletter status check, pricing availability, or personalization flags.
- For the core content, use Server Components and Next.js caching and skip client caching libraries in many cases.
Why it matters:
- Bundle size and hydration time matter more than client caching sophistication on content pages.
# Migration Tips: SWR to React Query and React Query to SWR#
Migration is less about swapping hooks and more about adopting the caching model.
Migrating from SWR to React Query#
What changes:
- Keys move from arbitrary strings to structured query keys, usually arrays.
- Revalidation becomes invalidation and refetching, which is more explicit.
- Mutations become
useMutationwith lifecycle handlers.
Practical steps:
- 1Create a query key convention, for example
["users", "list", filters]and["users", "detail", id]. - 2Wrap the app in a single QueryClientProvider in
app/providers.tsx. - 3Replace critical SWR hooks first, starting with endpoints that cause the most double fetching or inconsistent updates.
- 4Replace SWR
mutatecalls withuseMutationplusinvalidateQueries.
Suggested mapping:
| Concept | SWR | React Query |
|---|---|---|
| Read hook | useSWR(key, fetcher) | useQuery({ queryKey, queryFn }) |
| Global cache update | mutate(key) | queryClient.invalidateQueries({ queryKey }) |
| Optimistic update | mutate(key, data, false) | onMutate plus setQueryData |
| Prefill from server | SWRConfig fallback | dehydrate plus HydrationBoundary |
Migrating from React Query to SWR#
This usually happens when:
- The app is read-heavy and you want less abstraction.
- You want to reduce bundle size and mental overhead.
- You rarely do mutations or can handle them in isolated places.
Practical steps:
- 1Audit query key usage and simplify into stable string keys.
- 2Replace invalidation patterns with explicit
mutatecalls on the relevant keys. - 3Re-evaluate stale policies: SWR defaults may revalidate more often than you expect.
- 4If you relied on Devtools for debugging, add lightweight logging around your fetchers and mutation flows.
⚠️ Warning: If your app depends on complex invalidation, moving from React Query to SWR can increase bug risk. Teams often miss one of several related keys, leaving stale UI in edge flows like bulk edit or pagination.
# Pricing, Maintenance, and Ecosystem Fit#
Both libraries are mature and widely used.
- SWR is maintained by Vercel and aligns well with the Next.js ecosystem.
- React Query is part of TanStack and is widely used across React and beyond.
From a risk standpoint, both are safe choices. The deciding factors are app complexity and how much you want the library to manage for you.
# Key Takeaways#
- Avoid double fetching by fetching once and hydrating the client cache: React Query uses dehydrate and HydrationBoundary, SWR uses SWRConfig fallback.
- Choose React Query for dashboards and SaaS apps with frequent mutations, complex invalidation, and optimistic updates.
- Choose SWR for simple read-heavy client fetching or small interactive widgets inside content-first Next.js pages.
- Unify cache keys and URLs across server and client, otherwise hydration will not prevent duplicate requests.
- In App Router, treat Server Components and Next.js fetch caching as the primary mechanism for initial data, and use client caching libraries for interactivity after hydration.
# Conclusion#
React Query and SWR both work in Next.js App Router, but they optimize for different realities. If your app is mutation-heavy or has a complex query graph, React Query will save engineering time and reduce stale UI bugs. If your needs are mostly read-only and you want the simplest tool that works, SWR is often the faster path.
If you want help choosing the right approach, fixing double fetching, or setting up a scalable caching strategy for App Router, contact Samioda and we will review your current data flow and propose a concrete implementation plan.
FAQ
More in Web Development
All →Next.js File Uploads Done Right: Direct-to-S3 and Cloudflare R2 with Presigned URLs, Validation, and Security
A practical 2026 guide to building secure, reliable direct-to-object-storage uploads in Next.js App Router using presigned URLs, server-side validation, retry handling, and optional antivirus scanning.
Next.js Background Jobs in 2026: Queues, Cron, and Long-Running Tasks on Vercel (and Beyond)
A practical guide to running background work in Next.js in 2026: Vercel Cron, serverless limits, queues with Upstash and Redis, and worker services for long-running tasks. Includes decision criteria, architecture diagrams, and a production checklist.
Next.js i18n with the App Router: Localized Routing, SEO, and Content Workflows (2026 Guide)
Implement Next.js i18n in the App Router with localized routing, language detection, SEO-safe metadata, and scalable translation workflows for JSON, CMS, or localization platforms.
Need help with your project?
We build custom solutions using the technologies discussed in this article. Senior team, fixed prices.
Related Articles
Next.js Caching Strategies Explained: SSR, SSG, ISR, Route Cache, and SWR
A practical guide to Next.js caching strategies in the App Router era — how SSR, SSG, ISR, the Route Cache, Data Cache, and SWR fit together, with decision tables, code examples, and common pitfalls like stale auth and tenant data.
Next.js App Router Migration Checklist (From Pages Router) + Common Pitfalls
A practical, step-by-step Next.js App Router migration plan from Pages Router, including a checklist for routing, data fetching, SEO metadata, deployment, and a troubleshooting guide for common pitfalls.
Website Performance Optimization: The Complete Checklist (Next.js + Core Web Vitals) for 2026
A practical, production-ready checklist for website performance optimization in Next.js: Core Web Vitals, images, lazy loading, CDN, and caching—plus before/after metrics and copy-paste config.