Pobieranie danych w Next.js — fetch, cache i rewalidacja

Jak naprawdę działa pobieranie danych w Next.js 16? Fetch, Data Cache, rewalidacja, updateTag, use cache i wzorce, które mają sens w App Router.

Opublikowano

1 grudnia 2025 12:49

Czytanie

6 min czytania

Aktualizacja

15 kwietnia 2026 11:52

Pobieranie danych w App Router Next.js to jeden z najbardziej mylących tematów dla developerów przechodzących z Pages Router lub innych frameworków. Zapomnij o getStaticProps i getServerSideProps — teraz masz fetch z rozszerzeniami, cache, i rewalidację. Nie wiesz, który router wybrać? Sprawdź App Router vs Pages Router.

W tym artykule wyjaśnię, jak to wszystko działa. Bez uproszczeń, z praktycznymi przykładami i wzorcami, które stosuję w produkcyjnych projektach.

Krótka odpowiedź: W Next.js 15+ fetch domyślnie NIE cachuje (zmiana względem wersji 13/14). Cachowanie jawne: cache: 'force-cache' (statyczne), cache: 'no-store' (dynamiczne), next: { revalidate: 60 } (ISR, czyli Incremental Static Regeneration, pozwala odświeżać strony statyczne po czasie bez pełnego rebuildu.). Rewalidacja na żądanie: revalidateTag(tag, 'max') lub revalidatePath(path). Dane z bazy/SDK: unstable_cache() lub nowsze use cache. Równoległe pobieranie: Promise.all(). Deduplikacja w Server Components: cache() z React. Streaming: <Suspense>.

Podstawy — fetch w Server Components

W App Router pobierasz dane bezpośrednio w komponencie. Żadnych specjalnych funkcji, żadnych hooków — po prostu async/await:

Code
// app/posts/page.tsx
async function PostsPage() {
  const res = await fetch('https://api.example.com/posts')
  const posts = await res.json()
 
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>{post.title}</li>
      ))}
    </ul>
  )
}
 
export default PostsPage

To działa, bo komponenty w App Router są domyślnie Server Components — wykonują się na serwerze, gdzie fetch jest dostępny globalnie.

Domyślne zachowanie cache

Next.js rozszerza natywny fetch o własny Data Cache. I tu zaczyna się zamieszanie, bo trzeba odróżnić cache danych od prerenderowanego HTML.

W Next.js 15+ fetch domyślnie nie zapisuje odpowiedzi do Data Cache. Jeśli chcesz cachować wynik requestu, podajesz to jawnie:

Code
// Domyślnie: bez Data Cache
const res = await fetch('https://api.example.com/posts')
 
// Jawne cachowanie odpowiedzi
const res = await fetch('https://api.example.com/posts', {
  cache: 'force-cache',
})

To jednak nie znaczy, że każda strona z takim fetch() będzie zawsze renderowana od zera przy każdym wejściu. Next.js nadal może prerenderować trasę i cachować HTML, jeśli cała reszta drzewa na to pozwala.

Ważne: W Next.js 13/14 domyślne zachowanie fetch było inne. Jeśli czytasz starsze tutoriale, bardzo łatwo pomylić stare przykłady z aktualnym modelem App Router.

Opcje cache w fetch

Next.js dodaje do fetch opcję cache i next.revalidate:

1. Static Data

Code
// Cachowane na zawsze — odpowiednik bardzo statycznych danych
const res = await fetch('https://api.example.com/posts', {
  cache: 'force-cache',
})

Użyj dla: treści, które rzadko się zmieniają (about page, FAQ, konfiguracja).

2. Dynamic Data

Code
// Nigdy nie cachowane — jak getServerSideProps
const res = await fetch('https://api.example.com/posts', {
  cache: 'no-store',
})

Użyj dla: danych specyficznych dla użytkownika, danych zmieniających się przy każdym żądaniu.

3. Time-based Revalidation (ISR)

Code
// Cachowane, ale odświeżane co 60 sekund
const res = await fetch('https://api.example.com/posts', {
  next: { revalidate: 60 },
})

Użyj dla: blogów, list produktów, treści aktualizowanych regularnie.

Rewalidacja — odświeżanie cache

Masz dwie strategie rewalidacji:

Time-based Revalidation

Cache wygasa po określonym czasie:

Code
// Rewalidacja co godzinę
const res = await fetch('https://api.example.com/posts', {
  next: { revalidate: 3600 },
})

Jak to działa:

  1. Użytkownik A odwiedza stronę → dane z cache (jeśli istnieją) lub świeże
  2. Mija 3600 sekund
  3. Użytkownik B odwiedza stronę → dostaje stare dane, ale w tle Next.js pobiera nowe
  4. Użytkownik C odwiedza stronę → dostaje nowe dane

To tzw. "stale-while-revalidate" — użytkownik zawsze dostaje odpowiedź szybko.

On-demand Revalidation

Ręczne odświeżanie cache przez API, czyli Application Programming Interface, definiuje sposób komunikacji między aplikacjami lub modułami.:

Code
// app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from 'next/cache'
import { NextRequest } from 'next/server'
 
export async function POST(request: NextRequest) {
  const { path, tag } = await request.json()
 
  if (path) {
    revalidatePath(path)
    return Response.json({ revalidated: true, path })
  }
 
  if (tag) {
    revalidateTag(tag, 'max')
    return Response.json({ revalidated: true, tag })
  }
 
  return Response.json({ revalidated: false })
}

Wywołaj po aktualizacji danych w CMS, czyli Content Management System, to system do zarządzania treścią bez ręcznej edycji kodu.:

Code
curl -X POST https://example.com/api/revalidate \
  -H "Content-Type: application/json" \
  -d '{"path": "/blog"}'

Taki endpoint musi być zabezpieczony tokenem albo podpisem webhooka. Bez tego otwierasz każdemu możliwość ręcznego czyszczenia cache.

Cache Tags — precyzyjna kontrola

Tagi pozwalają grupować dane i rewalidować je razem:

Code
// Pobieranie z tagiem
async function getPosts() {
  const res = await fetch('https://api.example.com/posts', {
    next: { tags: ['posts'] },
  })
  return res.json()
}
 
async function getPost(id: string) {
  const res = await fetch(`https://api.example.com/posts/${id}`, {
    next: { tags: ['posts', `post-${id}`] },
  })
  return res.json()
}
Code
// Rewalidacja wszystkich postów
import { revalidateTag } from 'next/cache'
 
revalidateTag('posts', 'max') // oznacza dane jako stale i odświeży je przy kolejnym odczycie
 
// Rewalidacja jednego posta
revalidateTag('post-123', 'max') // odświeża tylko ten post

Jeśli wywołujesz mutację wewnątrz Server Action i chcesz od razu zobaczyć własny zapis, sprawdź też updateTag(). To bardziej precyzyjne API dla scenariusza "read your own writes".

Pobieranie danych bez fetch

Nie wszystkie dane pochodzą z HTTP API. Co z bazą danych, systemem plików, zewnętrznymi SDK?

Baza danych (Prisma, Drizzle) — sprawdź pełny tutorial Prisma + Next.js

Code
import { db } from '@/lib/database'
import { unstable_cache } from 'next/cache'
 
// Bez cache — świeże dane przy każdym żądaniu
async function getUsers() {
  return db.user.findMany()
}
 
// Z cache
const getCachedUsers = unstable_cache(
  async () => db.user.findMany(),
  ['users'], // klucz cache
  { revalidate: 3600, tags: ['users'] }
)

unstable_cache nadal działa, ale w aktualnej dokumentacji Next.js jest traktowane jako starsze API. W nowych projektach warto śledzić też use cache, cacheTag() i Cache Components, bo to w tę stronę idzie framework.

Zewnętrzne SDK

Code
import { unstable_cache } from 'next/cache'
import { stripe } from '@/lib/stripe'
 
const getProducts = unstable_cache(
  async () => {
    const products = await stripe.products.list({ limit: 100 })
    return products.data
  },
  ['stripe-products'],
  { revalidate: 60 }
)

Wzorce pobierania danych

Wzorzec 1: Parallel fetching

Pobieraj niezależne dane równolegle:

Code
// ❌ Źle — sekwencyjne, wolne
async function Dashboard() {
  const user = await getUser()
  const posts = await getPosts()
  const stats = await getStats()
  // ...
}
 
// ✅ Dobrze — równoległe, szybkie (więcej o Promise.all w artykule o [async/await](/blog/async-await-to-pulapka-kiedy-promise-all-uratuje-twoja-wydajnosc/))
async function Dashboard() {
  const [user, posts, stats] = await Promise.all([
    getUser(),
    getPosts(),
    getStats(),
  ])
  // ...
}

Wzorzec 2: Preloading

Zacznij pobierać dane wcześniej:

Code
// lib/data.ts
import { db } from '@/lib/database'
import { cache } from 'react'
 
export const getUser = cache(async (id: string) => {
  return db.user.findUnique({ where: { id } })
})
 
export const preloadUser = (id: string) => {
  void getUser(id)
}
Code
// app/user/[id]/page.tsx
import { getUser, preloadUser } from '@/lib/data'
 
export default async function UserPage({
  params,
}: {
  params: Promise<{ id: string }>
}) {
  const { id } = await params
  preloadUser(id) // zacznij pobierać od razu
  
  // ... inny kod ...
  
  const user = await getUser(id) // dane już (prawie) gotowe
  return <UserProfile user={user} />
}

Wzorzec 3: Streaming z Suspense

Pokazuj treść progresywnie:

Code
import { Suspense } from 'react'
 
async function SlowData() {
  const data = await fetch('https://slow-api.com/data', {
    cache: 'no-store',
  })
  return <div>{/* ... */}</div>
}
 
export default function Page() {
  return (
    <main>
      <h1>Dashboard</h1>
      
      {/* To pokazuje się od razu */}
      <QuickStats />
      
      {/* To streamuje się, gdy będzie gotowe */}
      <Suspense fallback={<Skeleton />}>
        <SlowData />
      </Suspense>
    </main>
  )
}

Wzorzec 4: Deduplikacja requestów

React automatycznie deduplikuje identyczne fetch w jednym renderze:

Code
// Ten sam zewnętrzny URL w różnych komponentach
async function Header() {
  const user = await fetch('https://api.example.com/user') // request 1
  return <div>{user.name}</div>
}
 
async function Sidebar() {
  const user = await fetch('https://api.example.com/user') // deduplikowany — używa request 1
  return <div>{user.email}</div>
}

Dla funkcji innych niż fetch użyj cache z React:

Code
import { cache } from 'react'
 
export const getUser = cache(async () => {
  return db.user.findFirst()
})
 
// Teraz getUser() jest deduplikowane w ramach jednego renderowania

Jeśli dane pochodzą z Twojej bazy lub własnego SDK, zwykle lepiej ominąć własny Route Handler i wywołać warstwę danych bezpośrednio na serwerze.

Dynamiczne vs Statyczne renderowanie

Next.js automatycznie decyduje, czy strona jest statyczna czy dynamiczna na podstawie użytych funkcji:

Strona będzie dynamiczna, gdy używasz:

  • fetch z cache: 'no-store'
  • cookies(), headers()
  • searchParams w komponencie strony
  • connection() lub innych dynamicznych API
Code
import { cookies } from 'next/headers'
 
// Ta strona będzie renderowana dynamicznie
async function Page() {
  const token = (await cookies()).get('token')
  // ...
}

Wymuszenie dynamicznego renderowania:

Code
// app/dashboard/page.tsx
export const dynamic = 'force-dynamic'
// lub
export const revalidate = 0
 
async function Dashboard() {
  // zawsze świeże dane
}

W nowszych wersjach Next.js zamiast starego unstable_noStore() częściej zobaczysz connection() albo po prostu jawne użycie cache: 'no-store' na poziomie konkretnego requestu.

Wymuszenie statycznego renderowania:

Code
// app/about/page.tsx
export const dynamic = 'force-static'
 
function About() {
  // zawsze z cache
}

Obsługa błędów

Code
async function PostsPage() {
  const res = await fetch('https://api.example.com/posts')
  
  if (!res.ok) {
    // Możesz throw error (złapie go error.tsx)
    throw new Error('Failed to fetch posts')
    
    // Lub obsłużyć gracefully
    return <div>Nie udało się załadować postów</div>
  }
  
  const posts = await res.json()
  return <PostsList posts={posts} />
}

Z error.tsx:

Code
// app/posts/error.tsx
'use client'
 
export default function Error({
  error,
  reset,
}: {
  error: Error
  reset: () => void
}) {
  return (
    <div>
      <h2>Coś poszło nie tak</h2>
      <p>{error.message}</p>
      <button onClick={reset}>Spróbuj ponownie</button>
    </div>
  )
}

FAQ

Jak działa cache w fetch Next.js i co zmieniło się w wersji 15?

W Next.js 13/14 fetch domyślnie zapisywał odpowiedź do Data Cache (force-cache). W Next.js 15+ domyślne zachowanie zmieniło się na brak zapisu do Data Cache, chyba że jawnie podasz cache: 'force-cache' lub next: { revalidate: X }. To ważna zmiana powodująca problemy przy migracji — starsze tutoriale opisują inne domyślne zachowanie.

Jaka jest różnica między revalidatePath a revalidateTag?

revalidatePath('/blog') czyści cache dla konkretnej ścieżki URL — przydatne gdy chcesz odświeżyć konkretną stronę. revalidateTag('posts', 'max') czyści cache dla wszystkich requestów otagowanych danym tagiem — niezależnie od tego, które strony z nich korzystają. Tagi są bardziej precyzyjne: możesz tagować ['posts', 'post-123'] i rewalidować tylko zmieniony post bez ruszania reszty.

Co to jest ISR (Incremental Static Regeneration) i jak go używać?

ISR to strategia cachowania, w której strona jest wstępnie zrenderowana statycznie, ale odświeżana w tle po upłynięciu zadanego czasu. Konfiguracja: fetch(url, { next: { revalidate: 3600 } }) lub export const revalidate = 3600 na poziomie strony. Mechanizm stale-while-revalidate: użytkownik zawsze dostaje cached odpowiedź (szybko), a Next.js regeneruje stronę w tle gdy cache wygaśnie.

Kiedy używać cache: 'no-store', a kiedy cache: 'force-cache'?

cache: 'no-store' — dane specyficzne dla użytkownika (sesja, koszyk, profil), dane zmieniające się przy każdym żądaniu, strony wymagające świeżych danych w czasie rzeczywistym. cache: 'force-cache' — treści rzadko się zmieniające (about page, FAQ, konfiguracja), publiczne dane, które mogą być wspólne dla wszystkich użytkowników. Domyślnie (bez opcji) Next.js 15+ zachowuje się jak no-store.

Jak cachować dane z bazy danych (Prisma, Drizzle) w Next.js?

Dane z bazy nie przechodzą przez rozszerzony fetch, więc używasz unstable_cache() z Next.js lub cache() z React. Przykład: const getCachedUsers = unstable_cache(async () => db.user.findMany(), ['users'], { revalidate: 3600, tags: ['users'] }). W aktualnej dokumentacji Next.js unstable_cache jest traktowane jako starsze API — nowe projekty mogą śledzić use cache jako kierunek przyszłościowy.

Jak pobierać dane równolegle w Next.js App Router?

Używaj Promise.all() dla niezależnych requestów: const [user, posts, stats] = await Promise.all([getUser(), getPosts(), getStats()]). Sekwencyjne await (jedno po drugim) sumuje czasy oczekiwania — przy trzech niezależnych requestach po 500ms różnica wynosi 1500ms vs 500ms. Dla preloadowania danych przed renderowaniem użyj wzorca preload() z void getUser(id) wywołanego wcześniej w drzewie komponentów.

Co to jest deduplikacja requestów w Next.js i jak działa?

React automatycznie deduplikuje identyczne wywołania fetch (ten sam URL, te same opcje) w jednym cyklu renderowania — każdy z nich wykonywany jest tylko raz, nawet jeśli wiele komponentów wywołuje go niezależnie. Dla funkcji nie-fetchowych (zapytania do bazy, SDK) użyj cache() z React: export const getUser = cache(async (id) => db.user.findUnique(...)) — deduplikuje wywołania w ramach jednego renderowania serwera.

Źródła i dokumentacja

Podsumowanie — cheat sheet

ScenariuszRozwiązanie
Dane statyczne (rzadko się zmieniają)fetch({ cache: 'force-cache' })
Dane dynamiczne (per-request)fetch({ cache: 'no-store' })
Dane odświeżane co X sekundfetch({ next: { revalidate: X } })
Odświeżanie po akcji (CMS, webhook)revalidatePath() lub revalidateTag(tag, 'max')
Natychmiastowy odczyt po mutacji w Server ActionupdateTag()
Dane z bazy/SDKunstable_cache() lub nowsze use cache
Równoległe pobieraniePromise.all([...])
Deduplikacjacache() z React
Streaming<Suspense>

Najczęstsze błędy

  1. Mieszanie cache danych z cache HTML — to dwa różne poziomy
  2. Zapominanie o cache: 'no-store' dla danych użytkownika
  3. Sekwencyjne zamiast równoległe pobieranie
  4. Brak obsługi błędów — zawsze sprawdzaj res.ok
  5. Wywoływanie własnych endpointów z Server Components bez potrzeby zamiast użycia warstwy danych
  6. Zbyt krótki revalidate — obciąża API bez potrzeby
  7. Zbyt długi revalidate — użytkownicy widzą stare dane

Chcesz głębiej poznać cachowanie? Przeczytaj o buforowaniu z unstable_cache i Redis lub sprawdź, jak działają Server Actions — formularze bez endpointów API.

Pracuję z tym zawodowo.

Jeśli chcesz przełożyć ten temat na lepszą architekturę frontendu, uporządkować React lub Next.js i podnieść jakość pracy zespołu, skontaktuj się ze mną. Pomagam zamieniać wiedzę z artykułów w praktyczne decyzje technologiczne.

O autorze

Maciej Sala

Maciej Sala — project manager i frontendowiec z doświadczeniem w marketingu internetowym. Na co dzień pracuję z Reactem, Next.js i TypeScriptem, łącząc perspektywę produktową z praktycznym podejściem do kodu. Przez kilka lat związany z branżą gier wideo jako project manager i game designer.

Absolwent historii na Uniwersytecie Jagiellońskim i studiów podyplomowych z marketingu internetowego na Akademii Górniczo-Hutniczej w Krakowie. Poza pracą trenuje na siłowni, maluje figurki i realizuje własne projekty.

Biblioteka wiedzy

Czytaj dalej

Zobacz więcej wpisów
Astro.js vs Next.js — które narzędzie wybrać w 2026 roku?

Astro.js vs Next.js — które narzędzie wybrać w 2026 roku?

Fachowe porównanie Astro.js i Next.js z perspektywy developera pracującego na co dzień w Next.js. Architektura, wydajność, SEO, DX, koszty i konkretne use case — z benchmarkami i przykładami kodu.

Maciej Sala

Maciej Sala

Founder Strivelab