Next.js: co to jest, App Router i komponenty serwerowe na przykładzie

Mateusz Bogolubow

03-11-2026

(aktualizacja: 03-11-2026)

Czym jest Next.js, czym różni się od Reacta i jak zacząć w App Routerze. Z uruchomionym projektem, komponentami serwerowymi i klienckimi oraz typowymi błędami z poprawkami.

Next.js: co to jest, App Router i komponenty serwerowe na przykładzie
Next.js to framework oparty na Reakcie, który dodaje do niego routing z folderów, renderowanie na serwerze i gotowe narzędzia do budowy i wdrożenia aplikacji. W App Routerze strony są domyślnie komponentami serwerowymi, a kod z interakcją (stan, kliknięcia) oznaczasz dyrektywą "use client". Zacznij od npx create-next-app@latest my-app --yes.

Pisząc ten artykuł, nie polegałem na pamięci. Next.js zmienia się szybko i kilka rzeczy, które możesz znać z poradników sprzed paru lat, działa dziś inaczej, dlatego oparłem się na dokumentacji dołączonej do wersji 16.4 i sprawdziłem każdy przykład w uruchomionym projekcie. Wersje: Next.js 16.4.4, React 19.3, Node.js w wersji co najmniej 20.9.

Zakładam, że znasz Reacta (komponenty, stan, hooki). Jeśli nie, zacznij od wyzwania czy jestem gotowy na Reacta. Kod przykładów jest w TypeScripcie, a wprowadzeniem do niego jest artykuł TypeScript jako kolejny krok po JavaScript.

Czym jest Next.js i czym różni się od samego Reacta?

React to biblioteka do budowania interfejsów. Nie mówi, jak zorganizować strony, jak pobierać dane na serwerze ani jak wdrożyć aplikację. Next.js to framework, który odpowiada na te pytania za Ciebie:

ObszarCzysty React (np. z Vite)Next.js
Routingdobierasz bibliotekę i konfigurujesz ją samstrony wynikają ze struktury folderów w app/
Renderowaniezwykle w przeglądarce (aplikacja jednostronicowa, SPA)na serwerze i w przeglądarce, z możliwością strumieniowania (streaming)
Pobieranie danychw przeglądarce (np. useEffect + fetch)także w komponencie na serwerze, blisko źródła danych
Wdrożenieserwer plików statycznychserwer Node.js, kontener Docker, eksport statyczny lub adaptery platform
Optymalizacjedodajesz samwbudowane m.in. obrazy, fonty i metadane (SEO)

Opis czystego Reacta jest ogólny (nie uruchamiałem tu Vite), a opis Next.js pochodzi z jego dokumentacji. Warto pamiętać, że Next.js potrafi też działać jako zwykła aplikacja jednostronicowa, więc nie musisz „wszystko albo nic”.

Kiedy wybrać Next.js, a kiedy wystarczy sam React?

  • Next.js ma sens, gdy ważne jest SEO i szybkie pierwsze wyświetlenie (treść jest w HTML od razu, o czym przekonasz się niżej), gdy aplikacja ma wiele stron albo gdy chcesz pobierać dane po stronie serwera.
  • Sam React wystarczy, gdy budujesz panel za logowaniem, prototyp albo małe narzędzie, a SEO nie gra roli.

W praktyce wielu pracodawców oczekuje znajomości Next.js po Reakcie. W ankiecie Stack Overflow z 2025 roku Next.js wskazuje 21,5% programistów zawodowych, o czym piszę w artykule co musi umieć junior frontend developer. Dlatego w naszym kursie jest on materiałem uzupełniającym po JavaScript i React, zobacz kurs frontend z mentorem.

Jak założyć pierwszy projekt w Next.js?

Najprostsza droga to narzędzie create-next-app:

npx create-next-app@latest my-app --yes
cd my-app
npm run dev

Flaga --yes pomija pytania i przyjmuje ustawienia domyślne: TypeScript, ESLint, App Router, Turbopack, alias importów @/* oraz plik AGENTS.md z instrukcjami dla narzędzi AI. Po uruchomieniu wejdź na http://localhost:3000.

W wygenerowanym przeze mnie projekcie (wersja 16.4.4) plik next.config.ts zawierał domyślnie dwie flagi: cacheComponents: true i partialPrefetching: true. Pierwsza z nich zmienia sposób, w jaki Next.js traktuje pobieranie danych na serwerze. Wracam do tego w sekcji o danych, bo to źródło błędu, który zobaczysz niemal na pewno.

Jak działa routing w App Routerze?

Strony definiujesz folderami w katalogu app/. Plik page.tsx tworzy stronę, a nawiasy kwadratowe w nazwie folderu tworzą trasę dynamiczną:

PlikAdres w przeglądarce
app/page.tsx/
app/posts/[id]/page.tsx/posts/1, /posts/2…

Dodatkowy plik layout.tsx opakowuje strony wspólnym układem (np. nagłówkiem i stopką). Nowy create-next-app dodaje już jeden, główny, w app/layout.tsx. Zwróć uwagę, że w tej wersji params w trasie dynamicznej to obietnica (Promise), więc w komponencie czekasz na nią słowem await. Wrócimy do tego w przykładzie.

Dane do przykładu to krótka lista wpisów, której pobranie udaje zapytanie do bazy (opóźnienie 300 ms):

// lib/posts.ts
export type Post = { id: string; title: string; likes: number };

const posts: Post[] = [
  { id: "1", title: "Czym jest Next.js", likes: 12 },
  { id: "2", title: "Komponenty serwerowe i klienckie", likes: 7 },
  { id: "3", title: "Routing z folderów", likes: 3 },
];

// Udaje zapytanie do bazy danych: wynik przychodzi po 300 ms.
export async function getPosts(): Promise<Post[]> {
  await new Promise((resolve) => setTimeout(resolve, 300));
  return posts;
}

export async function getPost(id: string): Promise<Post | undefined> {
  const all = await getPosts();
  return all.find((post) => post.id === id);
}

Czym różni się komponent serwerowy od klienckiego?

W App Routerze layouty i strony są domyślnie komponentami serwerowymi (Server Components). Renderują się na serwerze, mają dostęp do bazy danych i sekretów, a ich kod nie trafia do przeglądarki. Gdy potrzebujesz interakcji, tworzysz komponent kliencki (Client Component) i zaczynasz plik od dyrektywy "use client":

Potrzebujesz…Użyj
pobierać dane z bazy lub API blisko źródła, używać kluczy i sekretówkomponentu serwerowego
zmniejszyć ilość JavaScriptu wysyłanego do przeglądarkikomponentu serwerowego
stanu (useState), obsługi zdarzeń (onClick), useEffectkomponentu klienckiego
API przeglądarki (window, localStorage)komponentu klienckiego

Przykład: przycisk „Lubię to” ma stan i obsługę kliknięcia, więc jest komponentem klienckim:

// app/ui/like-button.tsx
"use client";

import { useState } from "react";

export default function LikeButton({ likes }: { likes: number }) {
  const [count, setCount] = useState(likes);

  return <button onClick={() => setCount(count + 1)}>Lubię to: {count}</button>;
}

Zwróć uwagę na ważną rzecz: komponent kliencki też renderuje się na serwerze przy pierwszym wejściu na stronę, a potem „ożywa” w przeglądarce (ten proces to hydration, czyli podpięcie obsługi zdarzeń do gotowego HTML-a). U mnie curl pobrał stronę /posts/1 i w samym HTML-u, bez uruchamiania JavaScriptu, był już tekst „Lubię to: 12”. Po kliknięciu w Chrome licznik zmienił się na 13.

Jak pobierać dane w Next.js i dlaczego build zgłasza błąd o Suspense?

Wystarczy, że komponent serwerowy jest funkcją asynchroniczną. Pierwsza wersja strony głównej wygląda naturalnie:

// app/page.tsx — wersja z błędem
import Link from "next/link";
import { getPosts } from "@/lib/posts";

export default async function Home() {
  const posts = await getPosts();

  return (
    <main>
      <h1>Blog</h1>
      <ul>
        {posts.map((post) => (
          <li key={post.id}>
            <Link href={`/posts/${post.id}`}>{post.title}</Link>
          </li>
        ))}
      </ul>
    </main>
  );
}

W trybie dev wszystko działa, ale npm run build kończy się błędem:

Error: Route "/": Next.js encountered uncached or runtime data during prerendering.

`fetch(...)`, `cookies()`, `headers()`, `params`, `searchParams`, or `connection()` accessed
outside of `<Suspense>` prevents the route from being prerendered, blocking the page load
and leading to a slower user experience.

Ways to fix this:
  - [stream] Provide a placeholder with `<Suspense fallback={...}>` around the data access
  - [cache] For uncached data (`fetch`, database calls): cache the access with `"use cache"`
  - [block] Set `export const instant = false` to allow a blocking route

To skutek włączonego domyślnie cacheComponents. Next.js próbuje wyrenderować z góry (prerendering) tyle strony, ile się da, a dane pobierane przy każdym żądaniu muszą mieć swoje „okienko” z zastępczą treścią. Błąd pokazuje trzy wyjścia: strumieniować (<Suspense>), zapisać w pamięci podręcznej ("use cache") albo świadomie zablokować trasę. Najczęstszy wybór to pierwszy.

Przenosimy pobieranie do osobnego komponentu i owijamy go w <Suspense>:

// app/ui/post-list.tsx
import Link from "next/link";
import { getPosts } from "@/lib/posts";

export default async function PostList() {
  const posts = await getPosts();

  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>
          <Link href={`/posts/${post.id}`}>{post.title}</Link>
        </li>
      ))}
    </ul>
  );
}
// app/page.tsx
import { Suspense } from "react";
import PostList from "@/app/ui/post-list";

export default function Home() {
  return (
    <main>
      <h1>Blog</h1>
      <Suspense fallback={<p>Ładowanie wpisów…</p>}>
        <PostList />
      </Suspense>
    </main>
  );
}

Nagłówek „Blog” trafia do użytkownika od razu, a lista dochodzi, gdy dane są gotowe. To samo robimy dla trasy dynamicznej, bo także params jest „danymi z żądania”:

// app/posts/[id]/page.tsx
import { Suspense } from "react";
import { notFound } from "next/navigation";
import LikeButton from "@/app/ui/like-button";
import { getPost } from "@/lib/posts";

async function PostDetails({ params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  const post = await getPost(id);
  if (!post) notFound();

  return (
    <>
      <h1>{post.title}</h1>
      <LikeButton likes={post.likes} />
    </>
  );
}

export default function PostPage({ params }: { params: Promise<{ id: string }> }) {
  return (
    <main>
      <Suspense fallback={<p>Ładowanie wpisu…</p>}>
        <PostDetails params={params} />
      </Suspense>
    </main>
  );
}

Teraz npm run build przechodzi, a podsumowanie tras pokazuje:

Route (app)
┌ ◐ /
├ ○ /_not-found
└   /posts/[id]
  └ ◐ /posts/[id]

○  (Static)             prerendered as static content
◐  (Partial Prerender)  prerendered as static HTML with dynamic server-streamed content

Znaczek ◐ oznacza „częściowe wyrenderowanie z góry”: statyczna powłoka strony plus treść dochodząca ze strumienia. Po npm run start pobrałem stronę główną przez curl i w HTML-u były już tytuły wpisów („Czym jest Next.js”, „Komponenty serwerowe i klienckie”), więc wyszukiwarka dostaje treść bez uruchamiania JavaScriptu. Ogólne zasady pobierania danych opisuje dokumentacja rozdziału o pobieraniu danych.

Jeszcze jedna pułapka z SEO, którą zobaczyłem w tym projekcie: wejście na /posts/99 (nieistniejący wpis, notFound() wywołane w <Suspense>) zwróciło kod HTTP 200, a nie 404, z tagiem <meta name="robots" content="noindex"/>. Dokumentacja wyjaśnia, że strumień ma już status 200, zanim notFound() się wykona, więc Next.js dodaje noindex, żeby strona nie trafiła do indeksu. Jeśli zależy Ci na prawdziwym kodzie 404, sprawdź to w swojej trasie.

Jakie błędy w Next.js są najczęstsze i jak je naprawić?

Dwa błędy z listy najpopularniejszych pytań na Stack Overflow odtworzyłem u siebie.

„You're importing a module that depends on useState…”

Komponent bez "use client", który używa useState:

// app/ui/broken-server.tsx — błąd
import { useState } from "react";

export default function BrokenServer() {
  const [count, setCount] = useState(0);
  return <button onClick={() => setCount(count + 1)}>Klik: {count}</button>;
}

Build kończy się komunikatem:

Error: You're importing a module that depends on `useState` into a React Server Component
module. This API is only available in Client Components. To fix, mark the file (or its
parent) with the `"use client"` directive.

Poprawka: dodaj "use client"; na samej górze pliku. Jeśli komponent jest duży, a stanu potrzebuje tylko jego fragment, wyodrębnij ten fragment (jak LikeButton) i zostaw resztę na serwerze.

„Hydration failed because the server rendered text didn't match the client”

Komponent kliencki, który na serwerze i w przeglądarce zwraca różny wynik:

// app/ui/browser-label.tsx — błąd
"use client";

export default function BrowserLabel() {
  // Na serwerze window nie istnieje, w przeglądarce tak: HTML się różni.
  const label = typeof window === "undefined" ? "serwer" : "przeglądarka";

  return <p>Wyrenderowano w: {label}</p>;
}

Serwer wysłał „serwer”, przeglądarka policzyła „przeglądarka”, więc React zgłasza błąd (w trybie dev w Chrome):

Hydration failed because the server rendered text didn't match the client. As a result this
tree will be regenerated on the client. This can happen if a SSR-ed Client Component used:

- A server/client branch `if (typeof window !== 'undefined')`.
- Variable input such as `Date.now()` or `Math.random()` which changes each time it's called.
- Date formatting in a user's locale which doesn't match the server.

Poprawka: pierwszy render ma być identyczny po obu stronach, a różnice wprowadza dopiero useEffect (uruchamia się tylko w przeglądarce):

// app/ui/browser-label.tsx — poprawnie
"use client";

import { useEffect, useState } from "react";

export default function BrowserLabel() {
  // Pierwszy render jest taki sam na serwerze i w przeglądarce.
  const [label, setLabel] = useState("serwer");

  useEffect(() => {
    setLabel("przeglądarka");
  }, []);

  return <p>Wyrenderowano w: {label}</p>;
}

Po tej zmianie błąd nie wystąpił, a na stronie pojawił się tekst „Wyrenderowano w: przeglądarka”. Podobne przyczyny mają hydration errors z Date.now(), Math.random() i formatowaniem dat w lokalnych ustawieniach użytkownika. Gdy trudno znaleźć źródło, pomaga debugowanie w DevTools.

Co zostało poza tym artykułem?

Wybrałem tylko rdzeń: routing, komponenty serwerowe i klienckie oraz dane. Nie omawiam tu Server Actions (formularze), Route Handlers (własne endpointy API), pamięci podręcznej z "use cache", optymalizacji obrazów i fontów ani wdrożenia. Dokumentacja Next.js opisuje je w osobnych rozdziałach, a wdrożenie na serwerze Node.js, w Dockerze, jako eksport statyczny albo przez adaptery platform omawia strona o wdrażaniu. Testowanie komponentów klienckich opisuje artykuł o React Testing Library; dla komponentów serwerowych nie sprawdzałem tu jeszcze żadnego podejścia, więc nic o nich nie obiecuję.

Podsumowanie

  • Next.js to framework na Reacta: routing z folderów, renderowanie na serwerze i narzędzia do wdrożenia.
  • Start: npx create-next-app@latest my-app --yes. Domyślnie TypeScript, App Router i cacheComponents.
  • Strony i layouty to komponenty serwerowe, a "use client" dodajesz tylko tam, gdzie potrzebujesz stanu, zdarzeń lub API przeglądarki.
  • Dane pobierane przy żądaniu owiń w <Suspense>, bo inaczej next build zgłosi błąd o prerenderingu.
  • notFound() w <Suspense> daje status 200 i noindex, a hydration error zwykle znaczy, że serwer i przeglądarka wyrenderowały co innego.
  • Jeśli chcesz uczyć się Next.js po Reaccie z code review, zajrzyj do kursu frontend z mentorem.
Konsultacja i warsztaty devmentor.pl

Masz pytania?

Partnerzy

Firmy, z którymi współpracujemy

To partnerzy odpowiedzialni za staże po naszych kursach. Organizują je dla najlepszych absolwentów.

Zobacz wszystkich partnerów →
  • VR Medical
  • Sophona
  • Robot Force