Suggest an editImprove this articleRefine the answer for “Data fetching in Next.js”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Data fetching in Next.js** App Router uses `async/await` directly in Server Components. No `getServerSideProps` or `getStaticProps`. ```tsx const data = await fetch('https://api.itlead.org/problems', { next: { revalidate: 300 } }).then(res => res.json()) ``` **Key point:** use `cache: 'no-store'` for user-specific data, `next: { revalidate: N }` for time-based ISR, and `Promise.all` to avoid sequential request waterfalls.Shown above the full answer for quick recall.Answer (EN)Image**Data fetching in Next.js App Router** uses `async/await` directly in Server Components, no special page functions required. ## Theory ### TL;DR - Server Components can be `async` functions; just `await` your data inside - `fetch` is extended by Next.js: cached by default, configurable per request - `cache: 'no-store'` = fresh every request (SSR). `next: { revalidate: N }` = stale-while-revalidate (ISR) - Sequential `await` creates waterfalls; use `Promise.all` for independent data - Client Components need `'use client'` + `useEffect` or SWR for interactive fetching ### Quick example ```tsx // app/dashboard/page.tsx export default async function Dashboard() { const data = await fetch('https://api.itlead.org/users', { next: { revalidate: 3600 } // Cached, refreshed every hour }).then(res => res.json()); return ( <ul> {data.map(user => <li key={user.id}>{user.name}</li>)} </ul> ); } // No useEffect. No useState. No client bundle for this component. ``` The component is `async`, so `await` works at the top level. Next.js runs it on the server, caches the result, and sends HTML to the client. ### How caching options map to rendering modes Before App Router, you picked a rendering mode per page: `getStaticProps` for SSG, `getServerSideProps` for SSR, `getStaticProps` with `revalidate` for ISR. Now all three translate to `fetch` options on individual requests: ```tsx // SSG equivalent: cached indefinitely (or until revalidated) const res = await fetch('https://api.itlead.org/docs') // SSR equivalent: fresh on every request const res = await fetch('https://api.itlead.org/feed', { cache: 'no-store' }) // ISR equivalent: cached, refreshed in background after N seconds const res = await fetch('https://api.itlead.org/problems', { next: { revalidate: 300 } }) // On-demand revalidation: tag the request, then call revalidateTag() const res = await fetch('https://api.itlead.org/problems', { next: { tags: ['problems'] } }) ``` This is a meaningful shift. You no longer choose caching at the page level. Each `fetch` call inside a component can have its own strategy, so a single page can mix static and dynamic data without any special configuration. ### Parallel fetching Sequential awaits create a waterfall. If `getUser` takes 200ms and `getStats` takes 300ms, sequential execution costs 500ms. Parallel costs 300ms. ```tsx // Bad: each awaits the previous export default async function DashboardPage() { const user = await getUser() const stats = await getStats() // waits for getUser const activity = await getActivity() // waits for getStats } // Good: all three fire at once export default async function DashboardPage() { const [user, stats, activity] = await Promise.all([ getUser(), getStats(), getActivity() ]) } ``` I've seen this waterfall pattern cause 3-4x slowdowns in dashboard pages where each data source was completely independent. It looks fine in code review and only shows up under real load. ### Preload pattern with React.cache `Promise.all` works when you fetch in one place. But when data is needed across a component tree, you want the request to start as early as possible and deduplicate if called more than once. ```tsx // lib/problems.ts import { cache } from 'react' export const getProblems = cache(async () => { const res = await fetch('https://api.itlead.org/problems') return res.json() }) export function preloadProblems() { void getProblems() // Starts the request, does not block } ``` ```tsx // app/problems/page.tsx import { getProblems, preloadProblems } from '@/lib/problems' export default async function ProblemsPage() { preloadProblems() // Kicks off the fetch early // ...render other stuff, then: const problems = await getProblems() // Already in-flight or cached } ``` `React.cache` guarantees the underlying request runs once per render, even if `getProblems` is called in multiple components. It is the React-native alternative to SWR deduplication, but for server-side code. ### Streaming with Suspense For slow data sources, wrapping an async component in `Suspense` lets the rest of the page render immediately. ```tsx // app/reports/page.tsx import { Suspense } from 'react' async function SlowChart() { const data = await fetch('https://api.itlead.org/analytics', { next: { tags: ['charts'] } }).then(r => r.json()) return <Chart data={data} /> } async function QuickHeader() { const stats = await db.stats.findFirst() return <header>Users: {stats.count}</header> } export default function Reports() { return ( <> <QuickHeader /> <Suspense fallback={<div>Loading chart...</div>}> <SlowChart /> </Suspense> </> ) } ``` `QuickHeader` renders and streams immediately. `SlowChart` streams in when its fetch resolves. No blocking. ### Client-side fetching For interactive features like search or filtering, server fetching does not work. The data depends on user input that does not exist at render time. ```tsx 'use client' import { useEffect, useState } from 'react' export function ProblemSearch() { const [query, setQuery] = useState('') const [results, setResults] = useState([]) useEffect(() => { if (!query) return const controller = new AbortController() fetch(`/api/search?q=${query}`, { signal: controller.signal }) .then(res => res.json()) .then(setResults) return () => controller.abort() // Cancel on query change }, [query]) return ( <div> <input value={query} onChange={e => setQuery(e.target.value)} /> <ul>{results.map(r => <li key={r.id}>{r.name}</li>)}</ul> </div> ) } ``` Load initial data on the server, pass it as props to Client Components, and use client-side fetching only for user-triggered updates. ### When to use which approach - Static content (docs, blog posts): `fetch` with default cache - User-specific data (profile, dashboard): `fetch` with `cache: 'no-store'` - Frequently updated content (leaderboard, prices): `next: { revalidate: N }` - CMS-managed content: `next: { tags: ['content'] }` + `revalidateTag` in a Server Action - Search, filtering, pagination: Client Component with `useEffect` or SWR - Slow data alongside fast UI: wrap in `Suspense` ### Common mistakes **Sequential awaits for independent data.** Already covered above, but it is the most common performance issue in App Router codebases. Always check if your awaits actually depend on each other. **Forgetting `cache: 'no-store'` for user-specific data.** ```tsx // Wrong: one cached response served to all users const user = await fetch(`/api/user/${id}`) // Right: fresh per request const user = await fetch(`/api/user/${id}`, { cache: 'no-store' }) ``` The default cache behavior means the same response could go to a different user. For any personalized data, always opt out. **Using `await fetch` in a Client Component at the top level.** ```tsx // Wrong: Client Components are not async functions 'use client' const data = await fetch('/api') // SyntaxError ``` Client Components cannot be `async`. Use `useEffect` or a library like SWR. **Mutating fetched data before returning it.** ```tsx // Wrong: mutates the cached object const data = await fetch(url).then(r => r.json()) data.push(extraItem) // Modifies shared cache reference ``` Next.js caches the response body. Mutating it affects every subsequent read from that cache. Clone first: `const data = structuredClone(await res.json())`. **Time-based revalidation when you need on-demand updates.** If content changes unpredictably (a CMS publish, a price change), `revalidate: 300` means up to 5 minutes of stale data. Use tags and `revalidateTag` in a Server Action or Route Handler instead. ### Real-world usage - T3 Stack (Next.js + tRPC + Prisma): Server Components with `await db.query()` for admin panels - Vercel Commerce: `fetch` Stripe APIs with `revalidate: 300` for product dashboards - Supabase examples: `createServerClient` in Server Components for auth and user data - Shadcn/UI docs: static `fetch` for MDX files at build time ### Follow-up questions **Q:** What is the difference between `next.revalidate` and `cache: 'no-store'`? **A:** `revalidate` caches the response and refreshes it in the background after N seconds (stale-while-revalidate). `no-store` skips the cache entirely and fetches fresh data on every request. **Q:** What happens if a `fetch` throws inside a Server Component? **A:** The error bubbles up to the nearest `error.tsx` file. Add an error boundary at the appropriate route segment to handle it. **Q:** How does `React.cache` differ from Next.js built-in `fetch` deduplication? **A:** Next.js automatically deduplicates identical `fetch` calls (same URL plus options) within a single request. `React.cache` works for any async function, not just `fetch`, so it covers Prisma queries and other data sources. **Q:** When should you use SWR or TanStack Query instead of `useEffect`? **A:** When you need automatic revalidation on focus, polling, optimistic updates, or complex cache management. `useEffect` is fine for simple one-off fetches; dedicated libraries handle edge cases better. **Q:** (Senior) How does Next.js handle `fetch` deduplication across `Suspense` boundaries? **A:** Identical `fetch` calls within a single RSC render are deduplicated via an in-memory cache scoped to the request. Even if two `Suspense`-wrapped components call the same URL concurrently, Next.js sends one actual network request and shares the result between them. ## Examples ### Basic: Server Component with a direct database query ```tsx // app/problems/page.tsx import { db } from '@/lib/db' export default async function ProblemsPage() { const problems = await db.problem.findMany({ orderBy: { difficulty: 'asc' } }) return ( <ul> {problems.map(p => ( <li key={p.id}>{p.name} ({p.difficulty})</li> ))} </ul> ) } // Runs on the server. The Prisma client never reaches the client bundle. ``` Direct database access in the component. No API route needed. The connection string and ORM stay server-side. ### Intermediate: Dashboard with parallel fetches and mixed caching ```tsx // app/admin/products/page.tsx import { db } from '@/lib/prisma' export default async function ProductsPage() { const [products, sales] = await Promise.all([ db.product.findMany({ include: { category: true } }), fetch('https://api.stripe.com/v1/analytics', { headers: { Authorization: `Bearer ${process.env.STRIPE_KEY}` }, next: { revalidate: 300 } // Stripe data refreshed every 5 min }).then(r => r.json()) ]) return ( <div> <h1>Products ({products.length})</h1> <pre>{JSON.stringify(sales.summary, null, 2)}</pre> </div> ) } // DB query and Stripe fetch run in parallel. // Stripe response is cached; DB query is always fresh. ``` Two data sources, two caching strategies, one parallel request. Prisma does not go through `fetch`, so Next.js does not cache it automatically. The Stripe call is. ### Advanced: Streaming dashboard with Suspense ```tsx // app/reports/page.tsx import { Suspense } from 'react' import { db } from '@/lib/db' async function SalesChart() { const data = await fetch('https://api.itlead.org/analytics/heavy', { next: { tags: ['charts'] } }).then(r => r.json()) return <Chart data={data} /> } async function QuickStats() { const stats = await db.stats.findFirst() return <p>Total users: {stats.count}</p> } export default function ReportsPage() { return ( <main> <QuickStats /> <Suspense fallback={<p>Loading chart...</p>}> <SalesChart /> </Suspense> </main> ) } // QuickStats renders and streams immediately. // SalesChart streams in when the slow fetch resolves. // No full-page blocking. ``` `ReportsPage` itself is not `async`. It just composes two async components. Each one manages its own data independently, and Suspense turns the slow chart into a non-blocking slot.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.