3. fetch() and Caching
Next.js caches `fetch()` permanently by default (easily leading to "stale data" bugs), which can be configured using `no-store` or `revalidate: N`. This cache operates exclusively in Server Components; React Query handles caching/polling in Client Components, and both are commonly used in tandem.
On this page
Summary
Next.js monkey-patches fetch() to cache results indefinitely by default (frequently causing "stale data" bugs), offering 3 control options: force-cache (default), no-store (always fresh), and revalidate: N (time-based cache, auto-refreshes after N seconds). This caching operates exclusively in Server Components. React Query solves an entirely different problem: client-side caching, polling, and refetching inside Client Components across user interactions. The two tools do not replace one another and are commonly paired: Server Components fetch initial data and pass it to Client Components as initialData for React Query to manage moving forward.
Context
Developers coming from pure React or the legacy Pages Router (fetch() inside useEffect, always retrieving fresh data) are often caught off guard when migrating to the App Router, as fetch() now exhibits an implicit, completely different default caching behavior.
1. fetch() defaults to indefinite caching (force-cache)
async function ProductPage() {
const res = await fetch('[https://api.example.com/products/5](https://api.example.com/products/5)');
const product = await res.json();
return <div>{product.name}</div>;
}
- By default (without explicit configuration), Next.js executes the API request only once and serves that cached payload to all subsequent users, even if the underlying data has changed. The value remains frozen until the application is rebuilt or redeployed.
- This is the primary root cause of the common "why is my data never updating?" bug when starting with the App Router.
2. { cache: 'no-store' }: completely disabling cache
const res = await fetch(url, { cache: 'no-store' });
- Fetches fresh data on every incoming request, mirroring legacy
getServerSidePropsbehavior. Suited for real-time or frequently fluctuating data (prices, inventory).
3. { next: { revalidate: N } }: time-based caching (fetch-level ISR)
const res = await fetch(url, { next: { revalidate: 3600 } }); // 3600 seconds = 1 hour
- Operates like
force-cache, but once N seconds elapse, the next incoming request triggers a fresh data fetch in the background (the requesting user immediately sees stale data without latency), and subsequent requests receive the refreshed data. This pattern is known as stale-while-revalidate. - Suited for low-churn data that still needs periodic updates (e.g. blog post feeds).
- The 3 options map directly to the 3 rendering strategies:
force-cache≈ SSG,no-store≈ SSR, andrevalidate≈ ISR.
4. React Query solves a different problem: client-side caching
- Next.js
fetch()caching lives strictly on the server, where every incoming request represents an isolated render lifecycle. It does not address client-side interaction needs:- Polling: automatically refreshing data at set intervals (real-time dashboards).
- Window focus refetching: updating data when the user focuses the browser tab.
- Optimistic updates: instantly updating the UI before receiving server confirmation.
- Session-level navigation caching: navigating back to a previously viewed view and serving it instantly from browser memory without re-querying the server.
5. Practical hybrid pattern
// page.tsx (Server Component)
export default async function DashboardPage() {
const initialData = await fetch('...', { cache: 'no-store' }).then(r => r.json());
return <OrderStats initialData="{initialData}"/>;
}
// OrderStats.tsx (Client Component)
'use client';
function OrderStats({ initialData }) {
const { data } = useQuery({
queryKey: ['orders-today'],
queryFn: fetchOrdersToday,
initialData,
refetchInterval: 10000,
});
return <div>{data.count} orders</div>;
}
- Core benefits: the user views metrics instantly (SEO-optimized, zero blank loading screens), React Query uses
initialDataas its baseline (skipping an immediate redundant fetch), and continuous background polling takes over seamlessly from there.
