Pytania rekrutacyjne Next.js

40 pytań — 20 odpowiedzi dostępnych od razu

Przygotuj się do rozmowy rekrutacyjnej z Next.js, korzystając z darmowego podglądu pytań i odpowiedzi. Materiał prezentuje formę powtórki na podstawie pełnego zestawu 40 zagadnień.

Zakres obejmuje architekturę Next.js, routing, renderowanie, pobieranie danych i optymalizację. Pytania pomagają wyjaśniać trade-offy między SSR, statycznym generowaniem i renderowaniem po stronie klienta.

Darmowy start

Pierwsze 20 odpowiedzi są dostępne od razu

Przeglądaj pełną listę 40 pytań. Pełne odpowiedzi, przykłady kodu i materiały do nauki odblokujesz w panelu.

Odblokuj 20 odpowiedzi

Next.js - Podstawy i Architektura

Czym jest Next.js i jakie problemy rozwiązuje w porównaniu do czystego React?

Odpowiedź w 30 sekund: Next.js to framework React oferujący gotowe rozwiązania dla SSR (Server-Side Rendering), SSG (Static Site Generation), routingu i optymalizacji. Rozwiązuje problemy czystego React takie jak SEO, początkowe ładowanie strony, konfiguracja routingu oraz brak wbudowanych mechanizmów renderowania po stronie serwera.

Odpowiedź w 2 minuty: Next.js to production-ready framework zbudowany na React, który dostarcza rozwiązania dla najczęstszych wyzwań w tworzeniu aplikacji webowych. Podczas gdy czysty React to biblioteka UI wymagająca dodatkowych narzędzi do routingu (React Router), SSR (własna konfiguracja), bundlingu (Webpack/Vite) i optymalizacji, Next.js oferuje to wszystko "out of the box".

Framework rozwiązuje kluczowe problemy: po pierwsze, SEO - dzięki renderowaniu po stronie serwera (SSR) i generowaniu statycznemu (SSG) content jest dostępny dla crawlerów. Po drugie, wydajność - automatyczne code splitting, optymalizacja obrazów przez komponent Image, prefetching linków. Po trzecie, developer experience - file-based routing eliminuje potrzebę konfiguracji tras, API routes pozwalają tworzyć backend endpoints w tym samym projekcie, a Fast Refresh zapewnia instant feedback podczas developmentu.

Next.js umożliwia również hybrydowe podejście - możesz mieszać SSG, SSR i CSR (Client-Side Rendering) w jednej aplikacji, wybierając optymalną strategię dla każdej strony. To czyni framework idealnym dla aplikacji wymagających zarówno dynamicznych jak i statycznych treści, e-commerce, dashboardów czy content-heavy websites.

Przykład kodu:

// Czysty React - wymaga dodatkowej konfiguracji routingu
import { BrowserRouter, Route } from 'react-router-dom';

function App() {
  return (
    <BrowserRouter>
      <Route path="/about" component={About} />
    </BrowserRouter>
  );
}

// Next.js - file-based routing, automatyczny SSR/SSG
// app/about/page.tsx
export default function About() {
  return <h1>O nas</h1>;
}

// Renderowanie po stronie serwera z danymi
export async function generateMetadata() {
  return {
    title: 'O nas',
    description: 'Dowiedz się więcej o naszej firmie'
  };
}

Materiały

Jakie są różnice między App Router a Pages Router w Next.js?

Odpowiedź w 30 sekund: Pages Router to klasyczny system routingu Next.js (katalog pages/), podczas gdy App Router to nowszy system (katalog app/) wprowadzony w Next.js 13. App Router wspiera React Server Components, ulepszone layouty, streaming oraz nowy model data fetching, podczas gdy Pages Router używa tradycyjnych metod jak getServerSideProps.

Odpowiedź w 2 minuty: Pages Router był głównym systemem routingu w Next.js od początku do wersji 12. Wykorzystuje katalog pages/, gdzie każdy plik automatycznie staje się routem. Data fetching odbywa się przez specjalne funkcje jak getStaticProps (SSG), getServerSideProps (SSR) i getInitialProps. Każda strona domyślnie jest Client Component, a optymalizacje wymagają manualnej konfiguracji.

App Router, wprowadzony w Next.js 13, to fundamentalna zmiana architektury. Wykorzystuje katalog app/ i wprowadza React Server Components jako domyślne - komponenty renderowane tylko po stronie serwera, co redukuje bundle size. Nowy model data fetching używa natywnego fetch() z automatycznym cache i revalidation. Layouty są teraz zagnieżdżone i współdzielone między routami, co eliminuje niepotrzebne re-rendery. Streaming i Suspense są wbudowane, pozwalając na progressive rendering.

Kluczowe różnice: w App Router domyślnie wszystko to Server Component (wymaga 'use client' dla interaktywności), podczas gdy Pages Router domyślnie to Client Component. App Router oferuje lepszą wydajność dzięki automatycznemu code splitting na poziomie komponentów, parallel routes, intercepting routes i loading UI. Pages Router pozostaje stabilny i dobrze udokumentowany, ale nie otrzymuje nowych features - to legacy approach.

Przykład kodu:

// PAGES ROUTER - pages/blog/[slug].tsx
export async function getServerSideProps({ params }) {
  const post = await fetchPost(params.slug);
  return { props: { post } };
}

export default function BlogPost({ post }) {
  return <article>{post.content}</article>;
}

// APP ROUTER - app/blog/[slug]/page.tsx
async function BlogPost({ params }) {
  // Fetch bezpośrednio w komponencie - Server Component
  const post = await fetchPost(params.slug);
  return <article>{post.content}</article>;
}

// Współdzielony layout dla wszystkich postów
// app/blog/layout.tsx
export default function BlogLayout({ children }) {
  return (
    <div>
      <nav>Menu bloga</nav>
      {children} {/* Layout persists między nawigacją */}
    </div>
  );
}
graph TB
    subgraph "Pages Router"
        A[pages/] --> B[index.tsx]
        A --> C[about.tsx]
        A --> D[blog/[slug].tsx]
        B --> E[getStaticProps]
        D --> F[getServerSideProps]
    end

    subgraph "App Router"
        G[app/] --> H[page.tsx]
        G --> I[layout.tsx]
        G --> J[blog/[slug]/page.tsx]
        J --> K[Server Component<br/>async/await]
        I --> L[Nested Layouts]
    end

Materiały

Czym są Server Components i Client Components w Next.js 13+?

Odpowiedź w 30 sekund: Server Components to komponenty React renderowane wyłącznie po stronie serwera, które nie trafiają do bundle JS przeglądarki. Client Components (oznaczone 'use client') to tradycyjne komponenty React z interaktywnością i hooks. Server Components domyślnie w App Router, redukują bundle size i mogą bezpośrednio odwoływać się do backendu.

Odpowiedź w 2 minuty: Server Components to rewolucyjna funkcja React zintegrowana z Next.js 13+, która zmienia sposób myślenia o renderowaniu. Komponenty te wykonują się tylko na serwerze, nigdy nie są wysyłane do przeglądarki jako JavaScript, co drastycznie redukuje bundle size. Mogą bezpośrednio odwoływać się do baz danych, API, filesystem - kod nie jest eksponowany klientowi. Nie mają dostępu do browser APIs, hooks stanu (useState, useEffect) ani event handlers.

Client Components to znane nam komponenty React - interaktywne, z dostępem do hooks, browser APIs i event handlers. W App Router wymagają dyrektywy 'use client' na początku pliku. Są niezbędne dla: formularzy, animacji, zarządzania stanem lokalnym, integracji z browser APIs (localStorage, geolocation), używania React hooks i third-party libraries wymagających przeglądarki.

Architektura hybrydowa: Next.js automatycznie optymalizuje granicę między server a client. Server Components mogą importować Client Components (ale nie odwrotnie), co pozwala na precyzyjne kontrolowanie co trafia do bundle. Najlepsza praktyka to Server Components jako domyślne, Client Components tylko tam gdzie potrzebna interaktywność. Server Components mogą przekazywać dane do Client Components przez props, eliminując potrzebę dodatkowych API calls.

Przykład kodu:

// app/dashboard/page.tsx - Server Component (domyślnie)
import { ClientCounter } from './ClientCounter';
import { db } from '@/lib/database';

export default async function Dashboard() {
  // Bezpośredni dostęp do bazy - kod NIE trafia do przeglądarki
  const users = await db.user.findMany();
  const stats = await calculateStats(); // Ciężkie obliczenia na serwerze

  return (
    <div>
      <h1>Dashboard</h1>
      {/* Server Component - zero JS w przeglądarce */}
      <UserList users={users} />
      {/* Client Component - interaktywny counter */}
      <ClientCounter initialCount={stats.totalVisits} />
    </div>
  );
}

// app/dashboard/ClientCounter.tsx - Client Component
'use client'; // Dyrektywa wymagana dla interaktywności

import { useState } from 'react';

export function ClientCounter({ initialCount }: { initialCount: number }) {
  const [count, setCount] = useState(initialCount);

  return (
    <button onClick={() => setCount(count + 1)}>
      Kliknięcia: {count}
    </button>
  );
}
graph LR
    A[Server Component] -->|może importować| B[Client Component]
    B -->|NIE może importować| A
    A -->|zero JS| C[Browser]
    B -->|wysyła JS| C
    A -->|dostęp| D[Database/API]
    A -->|dostęp| E[Filesystem]
    B -->|brak dostępu| D
    B -->|dostęp| F[Browser APIs]
    A -->|brak dostępu| F

Materiały

Kiedy używać dyrektywy "use client" a kiedy "use server"?

Odpowiedź w 30 sekund: 'use client' używaj dla komponentów wymagających interaktywności (hooks, event handlers, browser APIs). 'use server' oznacza Server Actions - funkcje wykonywane na serwerze, używane w formularzach i mutacjach danych. Domyślnie w App Router wszystko to Server Component, więc 'use client' dodajesz tylko gdy potrzebna interaktywność.

Odpowiedź w 2 minuty: 'use client' to dyrektywa oznaczająca Client Component boundary. Używaj gdy: (1) potrzebujesz React hooks (useState, useEffect, useContext), (2) obsługujesz zdarzenia użytkownika (onClick, onChange), (3) korzystasz z browser APIs (localStorage, window, document), (4) używasz third-party libraries wymagających przeglądarki, (5) tworzysz interaktywne UI (formularze, animacje, modals). Ważne: umieszczaj 'use client' możliwie najniżej w drzewie komponentów - nie oznaczaj całej strony jako client jeśli tylko mały komponent wymaga interaktywności.

'use server' oznacza Server Actions - funkcje asynchroniczne wykonywane wyłącznie na serwerze. Używaj gdy: (1) obsługujesz mutacje danych (POST, PUT, DELETE), (2) integrujesz z bazami danych, (3) wykonujesz wrażliwe operacje (np. z secret keys), (4) implementujesz form submissions, (5) revalidujesz cache. Server Actions mogą być wywoływane z Client Components przez form actions lub bezpośrednie wywołania, ale zawsze wykonują się na serwerze - nigdy nie eksponują kodu/secrets przeglądarce.

Strategia: rozpoczynaj od Server Components (domyślne), dodawaj 'use client' tylko gdy naprawdę potrzebne, wynoś interaktywne części do małych wydzielonych komponentów. Server Actions ('use server') używaj dla wszelkich operacji modyfikujących dane zamiast tradycyjnych API routes - są bezpieczniejsze i prostsze w obsłudze.

Przykład kodu:

// app/products/page.tsx - Server Component (domyślnie, bez dyrektyw)
import { ProductFilter } from './ProductFilter'; // Client Component
import { db } from '@/lib/db';

export default async function ProductsPage() {
  const products = await db.product.findMany(); // Bezpośredni dostęp do DB

  return (
    <div>
      <ProductFilter /> {/* Mały Client Component */}
      <ProductList products={products} /> {/* Server Component */}
    </div>
  );
}

// app/products/ProductFilter.tsx
'use client'; // Potrzebne: useState, onChange

import { useState } from 'react';

export function ProductFilter() {
  const [category, setCategory] = useState('all');

  return (
    <select onChange={(e) => setCategory(e.target.value)}>
      <option value="all">Wszystkie</option>
      <option value="electronics">Elektronika</option>
    </select>
  );
}

// app/actions.ts - Server Actions
'use server'; // Cały plik to Server Actions

import { db } from '@/lib/db';
import { revalidatePath } from 'next/cache';

export async function createProduct(formData: FormData) {
  // Wykonuje się NA SERWERZE, nawet gdy wywoływane z Client Component
  const name = formData.get('name') as string;

  await db.product.create({
    data: { name, price: 100 }
  });

  revalidatePath('/products'); // Odśwież cache
}

// app/products/NewProductForm.tsx
'use client'; // Formularz wymaga interaktywności

import { createProduct } from '../actions';

export function NewProductForm() {
  return (
    <form action={createProduct}>
      {/* createProduct wykonuje się na serwerze */}
      <input name="name" required />
      <button type="submit">Dodaj produkt</button>
    </form>
  );
}
flowchart TD
    A{Potrzebujesz interaktywności?} -->|Tak| B['use client']
    A -->|Nie| C[Server Component<br/>domyślnie]

    B --> D{Co dokładnie?}
    D -->|hooks, events, browser APIs| E[Client Component]

    F{Mutacje danych?} -->|Tak| G['use server'<br/>Server Action]
    F -->|Nie| C

    G --> H[Bezpieczne operacje<br/>na serwerze]

    style B fill:#ff9999
    style G fill:#99ccff
    style C fill:#99ff99

Materiały

Jak działa hydracja w Next.js i jakie są jej implikacje dla wydajności?

Odpowiedź w 30 sekund: Hydracja to proces, w którym React "ożywia" HTML wygenerowany na serwerze, dodając interaktywność i event listeners. W Next.js serwer wysyła gotowy HTML (szybkie First Contentful Paint), następnie JavaScript "hydratuje" go czyniąc interaktywnym. Błędy hydracji występują gdy HTML serwera nie zgadza się z renderem klienta.

Odpowiedź w 2 minuty: Hydracja w Next.js to kluczowy proces łączący Server-Side Rendering z interaktywnością React. Działa w trzech krokach: (1) Serwer renderuje komponenty React do HTML i wysyła go do przeglądarki - użytkownik widzi content natychmiast (FCP, First Contentful Paint). (2) Przeglądarka pobiera JavaScript bundle aplikacji. (3) React wykonuje "hydrację" - przechodzi przez istniejący DOM, dopasowuje go do Virtual DOM i attachuje event handlers, czyniąc stronę interaktywną (TTI, Time to Interactive).

Implikacje wydajnościowe są znaczące: po stronie pozytywów - użytkownik widzi content natychmiast zanim JavaScript się załaduje (lepsze UX, SEO), HTML jest funkcjonalny nawet bez JS (progressive enhancement). Po stronie negatywów - okres między FCP a TTI gdzie strona wygląda interaktywnie ale nie reaguje na kliknięcia ("uncanny valley"), podwójna praca - rendering na serwerze i ponownie w przeglądarce (CPU overhead), duże JavaScript bundles opóźniają TTI.

Next.js 13+ App Router wprowadza Selective Hydration i Streaming - komponenty mogą być hydratowane progresywnie w miarę potrzeby, Server Components w ogóle nie wymagają hydracji (zero JS), co drastycznie redukuje TTI. Partial Prerendering (PPR) pozwala mieszać statyczne i dynamiczne części bez hydracji całej strony.

Przykład kodu:

// Serwer generuje HTML
// app/page.tsx - Server Component
export default async function HomePage() {
  const data = await fetch('https://api.example.com/data');

  return (
    <div>
      <h1>Witaj!</h1>
      {/* Ten HTML jest wysyłany natychmiast */}
      <StaticContent data={data} />
      {/* Ten komponent wymaga hydracji */}
      <InteractiveButton />
    </div>
  );
}

// Client Component - wymaga hydracji
'use client';
import { useState } from 'react';

export function InteractiveButton() {
  const [count, setCount] = useState(0);

  // PRZED hydracją: button jest w DOM ale onClick nie działa
  // PO hydracji: onClick jest aktywny
  return (
    <button onClick={() => setCount(count + 1)}>
      Kliknięć: {count}
    </button>
  );
}

// Częsty błąd hydracji - różnica między serverem a klientem
export function ProblematicComponent() {
  // ❌ ZŁE - Date.now() da różne wartości na serwerze i kliencie
  return <div>Timestamp: {Date.now()}</div>;

  // ✅ DOBRE - useEffect wykonuje się tylko w przeglądarce
  const [timestamp, setTimestamp] = useState<number | null>(null);

  useEffect(() => {
    setTimestamp(Date.now());
  }, []);

  return <div>Timestamp: {timestamp ?? 'Loading...'}</div>;
}
sequenceDiagram
    participant User
    participant Browser
    participant Server
    participant React

    User->>Browser: Otwiera stronę
    Browser->>Server: Request
    Server->>Server: SSR - renderuje HTML
    Server->>Browser: HTML + CSS (FCP)
    Note over Browser: Użytkownik widzi content<br/>ale brak interaktywności

    Browser->>Server: Pobiera JS bundle
    Server->>Browser: JavaScript
    Browser->>React: Uruchamia React
    React->>React: Hydracja - attachuje event handlers
    Note over Browser: TTI - strona interaktywna

    Note over User,React: Czas FCP → TTI = "Uncanny Valley"

Materiały

Next.js - Routing

Jak działa system routingu oparty na plikach w App Router?

Odpowiedź w 30 sekund: App Router w Next.js 13+ wykorzystuje strukturę folderów w katalogu app/ do definiowania tras. Każdy folder reprezentuje segment URL, a specjalne pliki jak page.tsx, layout.tsx czy route.ts określają zachowanie danej trasy. System automatycznie tworzy routing na podstawie hierarchii katalogów.

Odpowiedź w 2 minuty: App Router to nowy system routingu wprowadzony w Next.js 13, który zastępuje poprzedni Pages Router. Opiera się na konwencji, gdzie struktura folderów w katalogu app/ bezpośrednio odpowiada strukturze URL aplikacji. Każdy folder w hierarchii reprezentuje segment ścieżki URL, tworząc intuicyjny i łatwy do zrozumienia system organizacji kodu.

Kluczowe pliki w systemie App Router to: page.tsx (definiuje UI dla konkretnej trasy i czyni ją publicznie dostępną), layout.tsx (wspólny szablon dla wielu stron), loading.tsx (UI stanu ładowania), error.tsx (obsługa błędów), oraz route.ts (API endpoints). System obsługuje także zagnieżdżone layouty, równoległe renderowanie i zaawansowane wzorce routingu.

App Router wykorzystuje React Server Components jako domyślne, co pozwala na renderowanie komponentów po stronie serwera, zmniejszając rozmiar bundle'a JavaScript wysyłanego do klienta. Nawigacja między trasami wykorzystuje preloadowanie i cache'owanie, zapewniając płynne przejścia podobne do SPA, zachowując jednocześnie korzyści renderowania po stronie serwera.

Routing jest natychmiastowy dzięki cache'owaniu po stronie klienta i prefetching - Next.js automatycznie wczytuje kod tras widocznych w viewport. Dodatkowo, stan aplikacji jest zachowywany podczas nawigacji, a odświeżane są tylko zmieniające się segmenty.

Przykład kodu:

// Struktura folderów w app/
// app/
//   ├── page.tsx              -> /
//   ├── about/
//   │   └── page.tsx          -> /about
//   ├── blog/
//   │   ├── page.tsx          -> /blog
//   │   ├── layout.tsx        -> Layout dla /blog/*
//   │   └── [slug]/
//   │       └── page.tsx      -> /blog/:slug
//   └── dashboard/
//       ├── layout.tsx
//       ├── page.tsx          -> /dashboard
//       └── settings/
//           └── page.tsx      -> /dashboard/settings

// app/page.tsx - Strona główna (/)
export default function HomePage() {
  return <h1>Strona główna</h1>;
}

// app/blog/page.tsx - Lista postów (/blog)
export default function BlogPage() {
  return <h1>Blog</h1>;
}

// app/blog/[slug]/page.tsx - Pojedynczy post (/blog/moj-post)
export default function BlogPost({ params }: { params: { slug: string } }) {
  return <h1>Post: {params.slug}</h1>;
}

// app/blog/layout.tsx - Wspólny layout dla wszystkich stron /blog/*
export default function BlogLayout({ children }: { children: React.ReactNode }) {
  return (
    <div>
      <nav>Blog Navigation</nav>
      {children}
    </div>
  );
}

Materiały

Czym są segmenty dynamiczne i jak je definiować w Next.js?

Odpowiedź w 30 sekund: Segmenty dynamiczne to parametry w URL, które pozwalają tworzyć trasy z zmiennymi wartościami. Definiuje się je poprzez umieszczenie nazwy folderu w nawiasach kwadratowych, np. [id] lub [slug]. Wartości tych parametrów są dostępne w komponencie przez obiekt params.

Odpowiedź w 2 minuty: Segmenty dynamiczne umożliwiają tworzenie elastycznych tras, które mogą obsługiwać różne wartości parametrów URL. Zamiast tworzyć osobną trasę dla każdego produktu, użytkownika czy posta, można utworzyć jedną trasę dynamiczną, która obsłuży wszystkie przypadki. W App Router definiuje się je przez nazwanie folderu w konwencji [parametr].

Next.js obsługuje kilka typów segmentów dynamicznych: pojedyncze segmenty ([id]), catch-all segmenty ([...slug]), które przechwytują wszystkie następujące segmenty URL, oraz opcjonalne catch-all segmenty ([[...slug]]), które działają jak catch-all, ale trasa działa także bez parametru. Wartości parametrów są automatycznie przekazywane do komponentów przez właściwość params.

Segmenty dynamiczne są szczególnie przydatne przy tworzeniu stron produktów, profili użytkowników, postów blogowych czy dowolnych treści generowanych dynamicznie. Można je łączyć ze statycznym generowaniem stron (SSG) używając funkcji generateStaticParams, co pozwala na pre-renderowanie określonych tras w czasie budowania aplikacji.

Next.js parsuje parametry URL i udostępnia je jako obiekt. Można mieć wiele segmentów dynamicznych w jednej ścieżce, a także łączyć segmenty statyczne z dynamicznymi, tworząc zaawansowane wzorce routingu.

Przykład kodu:

// Pojedynczy segment dynamiczny
// app/products/[id]/page.tsx -> /products/123
export default function ProductPage({ params }: { params: { id: string } }) {
  return <h1>Produkt ID: {params.id}</h1>;
}

// Wiele segmentów dynamicznych
// app/shop/[category]/[product]/page.tsx -> /shop/electronics/laptop
export default function ProductDetailPage({
  params
}: {
  params: { category: string; product: string }
}) {
  return (
    <div>
      <h1>Kategoria: {params.category}</h1>
      <h2>Produkt: {params.product}</h2>
    </div>
  );
}

// Catch-all segment (przechwytuje wszystkie segmenty)
// app/docs/[...slug]/page.tsx
// Obsługuje: /docs/a, /docs/a/b, /docs/a/b/c, itd.
// NIE obsługuje: /docs
export default function DocsPage({ params }: { params: { slug: string[] } }) {
  // params.slug będzie tablicą: ['a', 'b', 'c']
  return <h1>Dokumentacja: {params.slug.join('/')}</h1>;
}

// Opcjonalny catch-all segment
// app/blog/[[...slug]]/page.tsx
// Obsługuje: /blog, /blog/2023, /blog/2023/12, /blog/2023/12/post
export default function BlogPage({
  params
}: {
  params: { slug?: string[] }
}) {
  if (!params.slug) {
    return <h1>Wszystkie posty</h1>;
  }
  return <h1>Blog: {params.slug.join('/')}</h1>;
}

// Generowanie statycznych ścieżek w czasie budowania
// app/products/[id]/page.tsx
export async function generateStaticParams() {
  // Pobierz listę produktów z API lub bazy danych
  const products = await fetch('https://api.example.com/products').then(res => res.json());

  // Zwróć tablicę obiektów z parametrami
  return products.map((product: any) => ({
    id: product.id.toString(),
  }));
}

// TypeScript: Definiowanie typu dla props
type PageProps = {
  params: { id: string };
  searchParams: { [key: string]: string | string[] | undefined };
};

export default async function Page({ params, searchParams }: PageProps) {
  // params.id - parametr z URL
  // searchParams - query parameters (?sort=asc)
  return <div>Produkt: {params.id}</div>;
}

Materiały

Jak zaimplementować zagnieżdżone layouty w App Router?

Odpowiedź w 30 sekund: Zagnieżdżone layouty implementuje się przez tworzenie plików layout.tsx na różnych poziomach hierarchii folderów. Każdy layout opakowuje swoje komponenty potomne, a layouty są automatycznie komponowane - zewnętrzny layout zawiera wewnętrzny, tworząc hierarchię szablonów.

Odpowiedź w 2 minuty: Zagnieżdżone layouty to potężna funkcja App Router, która pozwala tworzyć hierarchiczne struktury szablonów. Każdy folder w hierarchii może mieć własny plik layout.tsx, który definiuje UI wspólny dla wszystkich tras w tym folderze i jego podfolderach. Layouty są automatycznie zagnieżdżane - layout nadrzędny owija layout potomny, który z kolei owija komponent strony.

Główna zaleta zagnieżdżonych layoutów to możliwość zachowania stanu między nawigacją - layout nie jest re-renderowany, gdy użytkownik przechodzi między stronami w tym samym layoutcie. To oznacza, że stan komponentów, pozycja scroll czy dane wejściowe są zachowywane. Ponadto, tylko zmieniające się części UI są aktualizowane, co znacznie poprawia wydajność.

Layouty mogą zawierać wspólną logikę, nawigację, sidebary, nagłówki czy stopki specyficzne dla danej sekcji aplikacji. Można też definiować różne metadane dla różnych sekcji strony. Root layout (app/layout.tsx) jest wymagany i musi zawierać tagi <html> i <body>.

W praktyce można mieć layout dla całej aplikacji (autentykacja, główna nawigacja), layout dla dashboardu (sidebar, breadcrumbs), layout dla sekcji bloga (kategorie, tagi) i tak dalej. Każdy poziom dodaje swoje elementy UI, tworząc kompletną strukturę strony.

Przykład kodu:

// app/layout.tsx - Root layout (wymagany)
// Opakowuje całą aplikację
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="pl">
      <body>
        <header>
          <nav>Główna nawigacja</nav>
        </header>
        {children}
        <footer>Stopka</footer>
      </body>
    </html>
  );
}

// app/dashboard/layout.tsx - Layout dla sekcji dashboard
// Opakowuje wszystkie trasy /dashboard/*
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
  return (
    <div className="dashboard-container">
      <aside className="sidebar">
        <nav>
          <a href="/dashboard">Przegląd</a>
          <a href="/dashboard/analytics">Analityka</a>
          <a href="/dashboard/settings">Ustawienia</a>
        </nav>
      </aside>
      <main className="content">
        {children}
      </main>
    </div>
  );
}

// app/dashboard/analytics/layout.tsx - Zagnieżdżony layout dla analityki
// Opakowuje wszystkie trasy /dashboard/analytics/*
export default function AnalyticsLayout({ children }: { children: React.ReactNode }) {
  return (
    <div className="analytics-container">
      <div className="analytics-header">
        <h1>Analityka</h1>
        <nav className="tabs">
          <a href="/dashboard/analytics/overview">Przegląd</a>
          <a href="/dashboard/analytics/reports">Raporty</a>
          <a href="/dashboard/analytics/exports">Eksporty</a>
        </nav>
      </div>
      <div className="analytics-content">
        {children}
      </div>
    </div>
  );
}

// app/dashboard/analytics/reports/page.tsx - Strona
// Ostateczna struktura DOM:
// RootLayout -> DashboardLayout -> AnalyticsLayout -> ReportsPage
export default function ReportsPage() {
  return <div>Zawartość raportów</div>;
}

// Struktura wizualna dla /dashboard/analytics/reports:
// ┌─────────────────────────────────────┐
// │ Header + Nav (RootLayout)           │
// ├─────────────┬───────────────────────┤
// │ Sidebar     │ Analytics Header      │
// │ (Dashboard  │ (AnalyticsLayout)     │
// │  Layout)    ├───────────────────────┤
// │             │ Reports Content       │
// │             │ (ReportsPage)         │
// │             │                       │
// └─────────────┴───────────────────────┘
// │ Footer (RootLayout)                 │
// └─────────────────────────────────────┘

// Przykład z metadanymi i stanami
// app/blog/layout.tsx
import { Metadata } from 'next';

export const metadata: Metadata = {
  title: {
    default: 'Blog',
    template: '%s | Blog', // Strony potomne mogą nadpisać
  },
};

export default function BlogLayout({ children }: { children: React.ReactNode }) {
  // Ten stan będzie zachowany podczas nawigacji między postami
  const [sidebarOpen, setSidebarOpen] = React.useState(true);

  return (
    <div className="blog-layout">
      <button onClick={() => setSidebarOpen(!sidebarOpen)}>
        Toggle Sidebar
      </button>
      {sidebarOpen && (
        <aside>
          <h3>Kategorie</h3>
          {/* Kategorie blogowe */}
        </aside>
      )}
      <main>{children}</main>
    </div>
  );
}

// Przykład z Template (alternatywa dla Layout)
// Template RE-RENDERUJE się przy każdej nawigacji
// app/dashboard/template.tsx
export default function DashboardTemplate({ children }: { children: React.ReactNode }) {
  // Ta animacja będzie uruchamiana przy każdej zmianie strony
  return <div className="fade-in">{children}</div>;
}

Materiały

Jakie jest zastosowanie plików loading.tsx i error.tsx?

Odpowiedź w 30 sekund: loading.tsx definiuje UI stanu ładowania, który jest automatycznie pokazywany podczas pobierania danych lub renderowania komponentów. error.tsx obsługuje błędy w danym segmencie trasy, wyświetlając fallback UI. Oba pliki tworzą granice React (Suspense Boundary i Error Boundary) automatycznie.

Odpowiedź w 2 minuty: Pliki loading.tsx i error.tsx to specjalne konwencje w App Router, które znacząco upraszczają obsługę asynchronicznych stanów i błędów. Są automatycznie integrowane z React Suspense i Error Boundaries, eliminując potrzebę ręcznego zarządzania tymi wzorcami.

loading.tsx tworzy Suspense Boundary, które otacza segment strony i jego dzieci. Gdy strona lub jej części są asynchronicznie renderowane (np. pobieranie danych z Server Components), Next.js automatycznie pokazuje UI z loading.tsx do momentu zakończenia ładowania. Można mieć różne stany ładowania dla różnych segmentów aplikacji, tworząc bardziej szczegółową informację zwrotną dla użytkownika. Loading UI jest natychmiast pokazywany z serwera jako część początkowego HTML (instant loading state).

error.tsx musi być Client Component i tworzy Error Boundary dla swojego segmentu. Automatycznie przechwytuje błędy w komponentach potomnych (w tym layoutach i stronach) i wyświetla fallback UI. Otrzymuje props error (obiekt błędu) i reset (funkcja do ponownej próby renderowania). Error boundaries izolują błędy - problem w jednej części aplikacji nie powoduje crash całej strony.

Oba mechanizmy wspierają streaming i progresywne renderowanie - użytkownik widzi części strony, które są gotowe, podczas gdy inne wciąż się ładują. To znacząco poprawia percepcję wydajności aplikacji. Można je komponować na różnych poziomach hierarchii, tworząc bardziej szczegółową obsługę stanów.

Przykład kodu:

// app/dashboard/loading.tsx
// Pokazywane podczas ładowania /dashboard i jego podstron
export default function DashboardLoading() {
  return (
    <div className="loading-container">
      <div className="spinner" />
      <p>Ładowanie dashboardu...</p>
    </div>
  );
}

// Bardziej zaawansowany loading z skeleton UI
// app/products/loading.tsx
export default function ProductsLoading() {
  return (
    <div className="products-grid">
      {[...Array(6)].map((_, i) => (
        <div key={i} className="product-card-skeleton">
          <div className="skeleton-image" />
          <div className="skeleton-title" />
          <div className="skeleton-price" />
        </div>
      ))}
    </div>
  );
}

// app/dashboard/error.tsx
// MUSI być Client Component!
'use client';

import { useEffect } from 'react';

export default function DashboardError({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  useEffect(() => {
    // Logowanie błędu do serwisu monitoringu (np. Sentry)
    console.error('Dashboard error:', error);
  }, [error]);

  return (
    <div className="error-container">
      <h2>Coś poszło nie tak!</h2>
      <p>{error.message}</p>
      <button
        onClick={() => reset()} // Próbuje ponownie wyrenderować segment
      >
        Spróbuj ponownie
      </button>
    </div>
  );
}

// Przykład strony, która korzysta z loading/error
// app/dashboard/page.tsx
async function getData() {
  const res = await fetch('https://api.example.com/data', {
    cache: 'no-store', // Wymusza dynamiczne pobieranie
  });

  if (!res.ok) {
    throw new Error('Nie udało się pobrać danych');
  }

  return res.json();
}

export default async function DashboardPage() {
  // Podczas ładowania danych, pokazywany jest loading.tsx
  const data = await getData();

  // Jeśli wystąpi błąd, pokazywany jest error.tsx
  return (
    <div>
      <h1>Dashboard</h1>
      <pre>{JSON.stringify(data, null, 2)}</pre>
    </div>
  );
}

// Zagnieżdżone loading states dla lepszego UX
// app/products/[id]/loading.tsx - Specyficzny dla szczegółów produktu
export default function ProductDetailLoading() {
  return <div>Ładowanie szczegółów produktu...</div>;
}

// Global error handler (obsługuje błędy w root layout)
// app/global-error.tsx
'use client';

export default function GlobalError({
  error,
  reset,
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  return (
    <html>
      <body>
        <h2>Wystąpił krytyczny błąd aplikacji!</h2>
        <button onClick={() => reset()}>Spróbuj ponownie</button>
      </body>
    </html>
  );
}

// Combining Suspense manually for more control
// app/dashboard/page.tsx
import { Suspense } from 'react';

async function SlowComponent() {
  await new Promise(resolve => setTimeout(resolve, 3000));
  return <div>Wolny komponent załadowany</div>;
}

async function FastComponent() {
  await new Promise(resolve => setTimeout(resolve, 500));
  return <div>Szybki komponent załadowany</div>;
}

export default function DashboardPage() {
  return (
    <div>
      {/* Szybki komponent załaduje się pierwszy */}
      <Suspense fallback={<div>Ładowanie szybkiego...</div>}>
        <FastComponent />
      </Suspense>

      {/* Wolny komponent nie blokuje szybkiego */}
      <Suspense fallback={<div>Ładowanie wolnego...</div>}>
        <SlowComponent />
      </Suspense>
    </div>
  );
}

// Hierarchia error handling:
// ┌─────────────────────────────────────┐
// │ global-error.tsx (root errors)      │
// │ ┌─────────────────────────────────┐ │
// │ │ app/error.tsx (app errors)      │ │
// │ │ ┌───────────────────────────┐   │ │
// │ │ │ dashboard/error.tsx       │   │ │
// │ │ │ (dashboard section errors)│   │ │
// │ │ └───────────────────────────┘   │ │
// │ └─────────────────────────────────┘ │
// └─────────────────────────────────────┘

Materiały

Czym są grupy tras (route groups) i do czego służą?

Odpowiedź w 30 sekund: Grupy tras to foldery w nawiasach, np. (marketing), które organizują kod bez wpływu na strukturę URL. Służą do grupowania logicznie powiązanych tras, tworzenia wielu layoutów dla różnych sekcji czy organizacji plików bez dodawania segmentów do ścieżki URL.

Odpowiedź w 2 minuty: Route groups to specjalna konwencja nazewnicza w App Router, gdzie folder w nawiasach okrągłych (np. (nazwa)) jest pomijany w ścieżce URL. To potężne narzędzie organizacyjne, które pozwala strukturyzować kod aplikacji bez wpływu na routing. Folder (marketing) nie pojawi się w URL - trasa app/(marketing)/about/page.tsx będzie dostępna pod /about, nie /marketing/about.

Główne zastosowania route groups to: organizacja kodu według funkcjonalności lub zespołów (marketing, sklep, dashboard), tworzenie wielu root layoutów dla różnych sekcji aplikacji (np. osobny layout dla publicznej strony i admina), grupowanie tras z podobnymi wymaganiami (np. wszystkie trasy wymagające autentykacji) oraz opcjonalne włączanie segmentów do URL tylko dla wybranych tras.

Szczególnie przydatne jest tworzenie wielu layoutów - można mieć (marketing)/layout.tsx dla stron publicznych i (app)/layout.tsx dla aplikacji webowej, każdy z własnym stylem, nawigacją i metadanymi. Next.js pozwala na wiele root layoutów, ale każdy musi zawierać tagi <html> i <body>, ponieważ tworzy osobne drzewo renderowania.

Route groups można także zagnieżdżać i łączyć z innymi konwencjami routingu. Można mieć grupy wewnątrz grup, grupy z segmentami dynamicznymi czy grupy z parallel routes. To daje ogromną elastyczność w organizacji dużych aplikacji, jednocześnie zachowując czyste i przewidywalne URL.

Przykład kodu:

// Struktura folderów z route groups
// app/
//   ├── (marketing)/           -> Nie wpływa na URL
//   │   ├── layout.tsx         -> Layout dla stron marketingowych
//   │   ├── page.tsx           -> / (strona główna)
//   │   ├── about/
//   │   │   └── page.tsx       -> /about
//   │   └── contact/
//   │       └── page.tsx       -> /contact
//   ├── (shop)/                -> Nie wpływa na URL
//   │   ├── layout.tsx         -> Layout dla sklepu
//   │   ├── products/
//   │   │   └── page.tsx       -> /products
//   │   └── cart/
//   │       └── page.tsx       -> /cart
//   └── (dashboard)/           -> Nie wpływa na URL
//       ├── layout.tsx         -> Layout dla dashboardu
//       ├── analytics/
//       │   └── page.tsx       -> /analytics
//       └── settings/
//           └── page.tsx       -> /settings

// app/(marketing)/layout.tsx - Layout dla stron marketingowych
export default function MarketingLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="pl">
      <body>
        <header className="marketing-header">
          <nav>
            <a href="/">Strona główna</a>
            <a href="/about">O nas</a>
            <a href="/contact">Kontakt</a>
          </nav>
        </header>
        <main>{children}</main>
        <footer className="marketing-footer">
          Marketing Footer
        </footer>
      </body>
    </html>
  );
}

// app/(shop)/layout.tsx - Inny layout dla sklepu
export default function ShopLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="pl">
      <body>
        <header className="shop-header">
          <nav>
            <a href="/products">Produkty</a>
            <a href="/cart">Koszyk</a>
          </nav>
        </header>
        <main className="shop-container">{children}</main>
        <footer className="shop-footer">
          Shop Footer
        </footer>
      </body>
    </html>
  );
}

// app/(dashboard)/layout.tsx - Layout dla dashboardu (wymaga auth)
import { redirect } from 'next/navigation';
import { getSession } from '@/lib/auth';

export default async function DashboardLayout({
  children
}: {
  children: React.ReactNode
}) {
  const session = await getSession();

  // Przekieruj jeśli nie zalogowany
  if (!session) {
    redirect('/login');
  }

  return (
    <html lang="pl">
      <body>
        <div className="dashboard-layout">
          <aside className="sidebar">
            <nav>
              <a href="/analytics">Analityka</a>
              <a href="/settings">Ustawienia</a>
            </nav>
          </aside>
          <main>{children}</main>
        </div>
      </body>
    </html>
  );
}

// Zagnieżdżone route groups
// app/
//   └── (auth)/                     -> /
//       ├── (public)/               -> /
//       │   ├── login/
//       │   │   └── page.tsx        -> /login
//       │   └── register/
//       │       └── page.tsx        -> /register
//       └── (protected)/            -> /
//           ├── layout.tsx          -> Sprawdza autentykację
//           ├── profile/
//           │   └── page.tsx        -> /profile
//           └── settings/
//               └── page.tsx        -> /settings

// Route groups z segmentami dynamicznymi
// app/
//   └── (shop)/
//       └── products/
//           └── [category]/         -> /products/:category
//               └── page.tsx

// Organizacja według funkcji zespołu
// app/
//   ├── (team-marketing)/
//   │   └── campaigns/
//   ├── (team-sales)/
//   │   └── leads/
//   └── (team-support)/
//       └── tickets/

// Opcjonalne segmenty URL
// app/
//   ├── (shop)/
//   │   ├── layout.tsx
//   │   └── products/
//   │       └── page.tsx            -> /products (bez /shop)
//   └── shop/
//       └── admin/
//           └── page.tsx            -> /shop/admin (z /shop)

// Użycie metadanych per grupa
// app/(marketing)/layout.tsx
import type { Metadata } from 'next';

export const metadata: Metadata = {
  title: 'Marketing Site',
  description: 'Strona marketingowa firmy',
};

export default function MarketingLayout({ children }: { children: React.ReactNode }) {
  return children;
}

// app/(dashboard)/layout.tsx
import type { Metadata } from 'next';

export const metadata: Metadata = {
  title: {
    default: 'Dashboard',
    template: '%s | Dashboard',
  },
  robots: {
    index: false, // Nie indeksuj stron dashboardu
  },
};

export default function DashboardLayout({ children }: { children: React.ReactNode }) {
  return children;
}

Materiały

Jak zaimplementować middleware w Next.js i jakie ma zastosowania?

Odpowiedź w 30 sekund: Middleware w Next.js to funkcja uruchamiana przed zakończeniem requestu, zdefiniowana w pliku middleware.ts w głównym katalogu projektu. Pozwala na modyfikację odpowiedzi, przekierowania, przepisywanie URL, dodawanie nagłówków czy autoryzację. Działa na Edge Runtime, zapewniając niskie opóźnienia.

Odpowiedź w 2 minuty: Middleware w Next.js to kod uruchamiany przed przetworzeniem requestu, działający na poziomie Edge Runtime (bliżej użytkownika końcowego). Definiuje się go przez eksport funkcji middleware z pliku middleware.ts umieszczonego w katalogu głównym projektu (obok app/ lub src/). Middleware otrzymuje obiekt NextRequest i może zwrócić NextResponse, modyfikując zachowanie aplikacji przed renderowaniem strony.

Główne zastosowania middleware to: autentykacja i autoryzacja (sprawdzanie tokenów, przekierowanie niezalogowanych użytkowników), internacjonalizacja (wykrywanie języka i przekierowanie), A/B testing i feature flags, logowanie i analityka, manipulacja nagłówkami (CORS, security headers), bot detection i rate limiting, oraz przepisywanie i przekierowania URL.

Middleware może działać na wszystkich trasach lub być ograniczone do określonych ścieżek przez konfigurację matcher lub warunkową logikę w funkcji. Działa przed cache Next.js, co oznacza, że może wpływać na to, czy i jak strony są cache'owane. Jest wykonywany na każdym requescie do pasujących tras, więc powinien być szybki i lekki.

Ważne ograniczenie: middleware działa na Edge Runtime, co oznacza, że nie wszystkie Node.js API są dostępne. Nie można używać natywnych modułów Node.js, systemu plików czy niektórych pakagów npm. Middleware jest idealny do szybkich operacji jak przekierowania, modyfikacja nagłówków czy proste sprawdzenia autentykacji, ale cięższe operacje powinny być w Server Components lub API Routes.

Przykład kodu:

// middleware.ts (w głównym katalogu projektu)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

// Podstawowy middleware
export function middleware(request: NextRequest) {
  // Logowanie requestu
  console.log('Request URL:', request.url);
  console.log('Request method:', request.method);

  // Kontynuuj normalnie
  return NextResponse.next();
}

// Konfiguracja - określa, na które trasy middleware reaguje
export const config = {
  matcher: [
    // Dopasuj wszystkie trasy poza static files, api, _next
    '/((?!api|_next/static|_next/image|favicon.ico).*)',
  ],
};

// Middleware z autentykacją
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  // Sprawdź, czy użytkownik jest zalogowany (token w cookies)
  const token = request.cookies.get('auth-token');
  const { pathname } = request.nextUrl;

  // Publiczne trasy dostępne dla wszystkich
  const publicPaths = ['/', '/login', '/register', '/about'];
  const isPublicPath = publicPaths.includes(pathname);

  // Jeśli brak tokenu i trasa chroniona, przekieruj do logowania
  if (!token && !isPublicPath) {
    const loginUrl = new URL('/login', request.url);
    loginUrl.searchParams.set('redirect', pathname); // Zachowaj docelowy URL
    return NextResponse.redirect(loginUrl);
  }

  // Jeśli zalogowany próbuje dostać się do /login, przekieruj do dashboard
  if (token && pathname === '/login') {
    return NextResponse.redirect(new URL('/dashboard', request.url));
  }

  return NextResponse.next();
}

export const config = {
  matcher: [
    '/((?!api|_next/static|_next/image|favicon.ico).*)',
  ],
};

// Middleware z internacjonalizacją (i18n)
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

const locales = ['en', 'pl', 'de'];
const defaultLocale = 'en';

function getLocale(request: NextRequest): string {
  // 1. Sprawdź cookie
  const cookieLocale = request.cookies.get('locale')?.value;
  if (cookieLocale && locales.includes(cookieLocale)) {
    return cookieLocale;
  }

  // 2. Sprawdź Accept-Language header
  const acceptLanguage = request.headers.get('accept-language');
  if (acceptLanguage) {
    const browserLocale = acceptLanguage.split(',')[0].split('-')[0];
    if (locales.includes(browserLocale)) {
      return browserLocale;
    }
  }

  return defaultLocale;
}

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // Sprawdź czy ścieżka już zawiera locale
  const pathnameHasLocale = locales.some(
    locale => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
  );

  if (pathnameHasLocale) return NextResponse.next();

  // Dodaj locale do URL
  const locale = getLocale(request);
  request.nextUrl.pathname = `/${locale}${pathname}`;

  return NextResponse.redirect(request.nextUrl);
}

export const config = {
  matcher: ['/((?!api|_next|.*\\..*).*)'],
};

// A/B Testing middleware
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // Tylko dla strony głównej
  if (pathname === '/') {
    // Sprawdź czy użytkownik już ma przypisany wariant
    let variant = request.cookies.get('ab-test-variant')?.value;

    if (!variant) {
      // Losowo przypisz wariant (50/50)
      variant = Math.random() < 0.5 ? 'A' : 'B';
    }

    // Przepisz URL na odpowiedni wariant
    const response = variant === 'B'
      ? NextResponse.rewrite(new URL('/variant-b', request.url))
      : NextResponse.next();

    // Ustaw cookie z wariantem
    response.cookies.set('ab-test-variant', variant, {
      maxAge: 60 * 60 * 24 * 30, // 30 dni
    });

    return response;
  }

  return NextResponse.next();
}

// Dodawanie security headers
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
  const response = NextResponse.next();

  // Dodaj security headers
  response.headers.set('X-Frame-Options', 'DENY');
  response.headers.set('X-Content-Type-Options', 'nosniff');
  response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
  response.headers.set(
    'Content-Security-Policy',
    "default-src 'self'; script-src 'self' 'unsafe-inline'"
  );

  return response;
}

// Rate limiting (prosty przykład)
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

// W produkcji użyj Redis lub innego store
const rateLimitMap = new Map<string, { count: number; resetTime: number }>();

export function middleware(request: NextRequest) {
  const ip = request.ip || 'unknown';
  const now = Date.now();
  const limit = 100; // Maksymalnie 100 requestów
  const window = 60 * 1000; // W ciągu 1 minuty

  const clientData = rateLimitMap.get(ip);

  if (!clientData || now > clientData.resetTime) {
    // Pierwszy request lub okno się zresetowało
    rateLimitMap.set(ip, {
      count: 1,
      resetTime: now + window,
    });
    return NextResponse.next();
  }

  if (clientData.count >= limit) {
    // Przekroczono limit
    return new NextResponse('Too Many Requests', {
      status: 429,
      headers: {
        'Retry-After': String(Math.ceil((clientData.resetTime - now) / 1000)),
      },
    });
  }

  // Zwiększ licznik
  clientData.count += 1;
  return NextResponse.next();
}

// Zaawansowany matcher config
export const config = {
  matcher: [
    // Dopasuj określone trasy
    '/dashboard/:path*',
    '/api/:path*',
    // Wyklucz static assets
    '/((?!_next/static|_next/image|favicon.ico).*)',
  ],
};

// Conditional middleware z wieloma funkcjami
// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

function authMiddleware(request: NextRequest) {
  // Logika autentykacji
  const token = request.cookies.get('token');
  if (!token) {
    return NextResponse.redirect(new URL('/login', request.url));
  }
}

function loggingMiddleware(request: NextRequest) {
  // Logowanie
  console.log(`${request.method} ${request.url}`);
}

export function middleware(request: NextRequest) {
  const { pathname } = request.nextUrl;

  // Zawsze loguj
  loggingMiddleware(request);

  // Autentykacja tylko dla /dashboard
  if (pathname.startsWith('/dashboard')) {
    const authResponse = authMiddleware(request);
    if (authResponse) return authResponse;
  }

  return NextResponse.next();
}

Materiały

Czym są parallel routes i intercepting routes?

Odpowiedź w 30 sekund: Parallel routes pozwalają renderować wiele stron jednocześnie w tym samym layoutcie, używając slotów oznaczonych @nazwa. Intercepting routes przechwytują nawigację do trasy i wyświetlają inny komponent (np. modal), zachowując oryginalny URL. Oba wzorce pozwalają tworzyć zaawansowane UI z zachowaniem kontekstu.

Odpowiedź w 2 minuty: Parallel routes i intercepting routes to zaawansowane wzorce routingu w App Router, które umożliwiają tworzenie skomplikowanych interfejsów użytkownika z wieloma równoległymi widokami i kontekstowym renderowaniem.

Parallel routes pozwalają renderować wiele stron jednocześnie w ramach tego samego layoutu. Definiuje się je przez foldery ze znakiem @ na początku nazwy (np. @team, @analytics). Każdy taki folder reprezentuje "slot", który jest przekazywany jako prop do layoutu rodzica. To pozwala na niezależne ładowanie, obsługę błędów i stany loading dla każdego slotu. Idealnie nadaje się do dashboardów z wieloma panelami, splitview, tabs czy conditional rendering.

Intercepting routes przechwytują nawigację do określonej trasy i renderują alternatywny komponent, najczęściej modal lub overlay, zachowując oryginalny URL i kontekst. Definiuje się je przez konwencję (..) w nazwie folderu - (.) to ten sam segment, (..) to jeden poziom wyżej, (..)(..) to dwa poziomy wyżej, (...) to root. Klasyczny przykład to Instagram-like interface: kliknięcie zdjęcia w feed otwiera modal z zachowaniem scroll position, ale bezpośrednie wejście na URL renderuje pełną stronę.

Oba wzorce można łączyć - intercepting routes mogą używać parallel routes do pokazania modalów jako slotów. To tworzy zaawansowane UX, gdzie nawigacja jest płynna i kontekstowa, ale deep linking wciąż działa. System zachowuje stan przy back/forward navigation i umożliwia różne renderowanie w zależności od sposobu dostępu (soft navigation vs hard navigation).

Przykład kodu:

// ============================================
// PARALLEL ROUTES
// ============================================

// Struktura folderów dla parallel routes
// app/
//   └── dashboard/
//       ├── layout.tsx
//       ├── @team/              -> Slot "team"
//       │   ├── page.tsx
//       │   └── loading.tsx
//       ├── @analytics/         -> Slot "analytics"
//       │   ├── page.tsx
//       │   └── error.tsx
//       └── page.tsx            -> Główna strona

// app/dashboard/layout.tsx
export default function DashboardLayout({
  children,
  team,      // Slot @team
  analytics, // Slot @analytics
}: {
  children: React.ReactNode;
  team: React.ReactNode;
  analytics: React.ReactNode;
}) {
  return (
    <div className="dashboard">
      <div className="main">{children}</div>
      <div className="sidebar">
        <section className="team-section">
          {team}
        </section>
        <section className="analytics-section">
          {analytics}
        </section>
      </div>
    </div>
  );
}

// app/dashboard/@team/page.tsx
export default function TeamSlot() {
  return (
    <div>
      <h2>Zespół</h2>
      <ul>
        <li>Jan Kowalski</li>
        <li>Anna Nowak</li>
      </ul>
    </div>
  );
}

// app/dashboard/@analytics/page.tsx
async function getAnalytics() {
  const res = await fetch('https://api.example.com/analytics');
  return res.json();
}

export default async function AnalyticsSlot() {
  const data = await getAnalytics();

  return (
    <div>
      <h2>Analityka</h2>
      <p>Wizyty: {data.visits}</p>
    </div>
  );
}

// Conditional rendering w parallel routes
// app/dashboard/layout.tsx
export default function DashboardLayout({
  children,
  team,
  analytics,
}: {
  children: React.ReactNode;
  team: React.ReactNode;
  analytics: React.ReactNode;
}) {
  const showAnalytics = true; // Może być oparte na permissions

  return (
    <div className="dashboard">
      {children}
      {team}
      {showAnalytics && analytics}
    </div>
  );
}

// Default slots (fallback gdy brak odpowiedniej trasy)
// app/dashboard/@team/default.tsx
export default function TeamDefault() {
  return <div>Wybierz zespół</div>;
}

// ============================================
// INTERCEPTING ROUTES
// ============================================

// Struktura dla intercepting routes (przykład typu Instagram)
// app/
//   ├── layout.tsx
//   ├── page.tsx                    -> Lista zdjęć
//   ├── photos/
//   │   └── [id]/
//   │       └── page.tsx            -> Pełna strona zdjęcia
//   └── @modal/                     -> Parallel route dla modala
//       ├── (..)photos/             -> Przechwytuje /photos/*
//       │   └── [id]/
//       │       └── page.tsx        -> Modal ze zdjęciem
//       └── default.tsx             -> Brak modala (pusty slot)

// app/layout.tsx
export default function RootLayout({
  children,
  modal, // Slot dla intercepted route
}: {
  children: React.ReactNode;
  modal: React.ReactNode;
}) {
  return (
    <html>
      <body>
        {children}
        {modal}  {/* Modal renderowany jako overlay */}
      </body>
    </html>
  );
}

// app/page.tsx - Lista zdjęć
import Link from 'next/link';

export default function Home() {
  const photos = [
    { id: 1, url: '/photo1.jpg' },
    { id: 2, url: '/photo2.jpg' },
  ];

  return (
    <div className="photo-grid">
      {photos.map(photo => (
        <Link key={photo.id} href={`/photos/${photo.id}`}>
          <img src={photo.url} alt={`Photo ${photo.id}`} />
        </Link>
      ))}
    </div>
  );
}

// app/photos/[id]/page.tsx - Pełna strona zdjęcia (hard navigation)
export default function PhotoPage({ params }: { params: { id: string } }) {
  return (
    <div className="photo-page">
      <h1>Zdjęcie {params.id}</h1>
      <img src={`/photo${params.id}.jpg`} alt={`Photo ${params.id}`} />
      <p>Pełna strona zdjęcia - bezpośredni link</p>
    </div>
  );
}

// app/@modal/(..)photos/[id]/page.tsx - Modal (soft navigation)
'use client';

import { useRouter } from 'next/navigation';

export default function PhotoModal({ params }: { params: { id: string } }) {
  const router = useRouter();

  return (
    <div className="modal-backdrop" onClick={() => router.back()}>
      <div className="modal-content" onClick={(e) => e.stopPropagation()}>
        <button onClick={() => router.back()}>✕ Zamknij</button>
        <img src={`/photo${params.id}.jpg`} alt={`Photo ${params.id}`} />
        <p>Modal - zachowany kontekst feed</p>
      </div>
    </div>
  );
}

// app/@modal/default.tsx - Pusty slot gdy nie ma modala
export default function ModalDefault() {
  return null;
}

// ============================================
// ZAAWANSOWANY PRZYKŁAD - Dashboard z modals
// ============================================

// app/
//   └── dashboard/
//       ├── layout.tsx
//       ├── page.tsx
//       ├── @stats/
//       │   └── page.tsx
//       ├── @modal/
//       │   ├── default.tsx
//       │   └── (.)settings/
//       │       └── page.tsx
//       └── settings/
//           └── page.tsx

// app/dashboard/layout.tsx
export default function DashboardLayout({
  children,
  stats,
  modal,
}: {
  children: React.ReactNode;
  stats: React.ReactNode;
  modal: React.ReactNode;
}) {
  return (
    <div className="dashboard-layout">
      <aside className="stats-sidebar">
        {stats}
      </aside>
      <main>
        {children}
      </main>
      {modal}  {/* Modal overlay */}
    </div>
  );
}

// app/dashboard/page.tsx
import Link from 'next/link';

export default function DashboardPage() {
  return (
    <div>
      <h1>Dashboard</h1>
      {/* Ten link otworzy modal przez intercepting route */}
      <Link href="/dashboard/settings">
        Otwórz ustawienia (modal)
      </Link>
    </div>
  );
}

// app/dashboard/@stats/page.tsx
export default function StatsSlot() {
  return (
    <div className="stats">
      <h3>Statystyki</h3>
      <p>Użytkownicy: 1,234</p>
      <p>Sprzedaż: 56,789 PLN</p>
    </div>
  );
}

// app/dashboard/@modal/(.)settings/page.tsx - Settings jako modal
'use client';

import { useRouter } from 'next/navigation';

export default function SettingsModal() {
  const router = useRouter();

  return (
    <div className="modal-overlay">
      <div className="modal">
        <h2>Ustawienia (Modal)</h2>
        <form>
          <input type="text" placeholder="Nazwa" />
          <input type="email" placeholder="Email" />
          <button type="submit">Zapisz</button>
        </form>
        <button onClick={() => router.back()}>Anuluj</button>
      </div>
    </div>
  );
}

// app/dashboard/settings/page.tsx - Settings jako pełna strona
export default function SettingsPage() {
  return (
    <div className="settings-page">
      <h1>Ustawienia (Pełna strona)</h1>
      <p>Bezpośredni dostęp do /dashboard/settings</p>
      <form>
        <input type="text" placeholder="Nazwa" />
        <input type="email" placeholder="Email" />
        <button type="submit">Zapisz</button>
      </form>
    </div>
  );
}

// Konwencje intercepting:
// (.)  - dopasowanie w tym samym segmencie
// (..) - dopasowanie jeden poziom wyżej (jak ../ w ścieżkach)
// (..)(..) - dwa poziomy wyżej
// (...) - root (app directory)

// Przykład różnych poziomów:
// app/
//   ├── feed/
//   │   └── @modal/
//   │       ├── (.)post/[id]/          -> Przechwytuje /feed/post/[id]
//   │       ├── (..)profile/[id]/      -> Przechwytuje /profile/[id]
//   │       └── (...)settings/         -> Przechwytuje /settings
//   ├── post/[id]/
//   ├── profile/[id]/
//   └── settings/

Materiały

Renderowanie w Next.js

13. Jakie strategie renderowania oferuje Next.js (SSR, SSG, ISR)?

Odpowiedź w 30 sekund: Next.js oferuje cztery główne strategie renderowania: Static Site Generation (SSG) - pre-renderowanie w czasie build, Server-Side Rendering (SSR) - renderowanie na każde żądanie, Incremental Static Regeneration (ISR) - aktualizacja statycznych stron w tle, oraz Client-Side Rendering (CSR) - renderowanie w przeglądarce. Każda strategia ma swoje zastosowania w zależności od wymagań dotyczących wydajności i świeżości danych.

Odpowiedź w 2 minuty: Next.js zapewnia elastyczny system renderowania, pozwalający wybrać najbardziej odpowiednią strategię dla każdej strony:

Static Site Generation (SSG) generuje HTML w czasie build. Strony są serwowane jako statyczne pliki, co zapewnia najlepszą wydajność. Idealny dla treści, które rzadko się zmieniają, jak blogi czy strony landingowe. W App Router używamy tego domyślnie, a w Pages Router przez getStaticProps.

Server-Side Rendering (SSR) renderuje HTML na każde żądanie użytkownika. Zapewnia zawsze aktualne dane, ale jest wolniejszy niż SSG. Używany dla spersonalizowanych treści lub szybko zmieniających się danych. W App Router osiągamy to przez fetch z cache: 'no-store', w Pages Router przez getServerSideProps.

Incremental Static Regeneration (ISR) łączy zalety SSG i SSR - strona jest statyczna, ale aktualizowana w tle w określonych odstępach czasu. Idealny dla treści, które zmieniają się regularnie, ale nie wymagają aktualizacji w czasie rzeczywistym.

Client-Side Rendering (CSR) renderuje zawartość w przeglądarce przy użyciu JavaScript. Używany dla interaktywnych części aplikacji lub dashboardów wymagających danych użytkownika.

Przykład kodu:

// App Router - różne strategie renderowania

// 1. SSG (domyślnie) - dane cache'owane
async function BlogPost({ params }) {
  const post = await fetch(`https://api.example.com/posts/${params.id}`)
    .then(res => res.json());

  return <article>{post.title}</article>;
}

// 2. SSR - dane zawsze świeże
async function Dashboard() {
  const data = await fetch('https://api.example.com/user', {
    cache: 'no-store' // Wymusza SSR
  }).then(res => res.json());

  return <div>{data.name}</div>;
}

// 3. ISR - rewalidacja co 60 sekund
async function Products() {
  const products = await fetch('https://api.example.com/products', {
    next: { revalidate: 60 } // ISR z rewalidacją
  }).then(res => res.json());

  return <div>{products.map(p => p.name)}</div>;
}

// 4. CSR - renderowanie po stronie klienta
'use client';
import { useState, useEffect } from 'react';

function UserProfile() {
  const [user, setUser] = useState(null);

  useEffect(() => {
    fetch('/api/user')
      .then(res => res.json())
      .then(setUser);
  }, []);

  return user ? <div>{user.name}</div> : <div>Ładowanie...</div>;
}
// Pages Router - klasyczne podejście

// SSG z getStaticProps
export async function getStaticProps() {
  const data = await fetch('https://api.example.com/data').then(r => r.json());

  return {
    props: { data },
    // revalidate: 60 // Dodanie tego tworzy ISR
  };
}

// SSR z getServerSideProps
export async function getServerSideProps(context) {
  const { req } = context;
  const data = await fetch('https://api.example.com/user', {
    headers: { cookie: req.headers.cookie }
  }).then(r => r.json());

  return {
    props: { data }
  };
}

// ISR - SSG + revalidate
export async function getStaticProps() {
  const posts = await fetch('https://api.example.com/posts').then(r => r.json());

  return {
    props: { posts },
    revalidate: 3600 // Rewalidacja co godzinę
  };
}

Materiały

14. Czym jest Incremental Static Regeneration (ISR) i jak go skonfigurować?

Odpowiedź w 30 sekund: ISR (Incremental Static Regeneration) to technika w Next.js, która pozwala aktualizować statyczne strony po ich zbudowaniu, bez przebudowywania całej aplikacji. Strona pozostaje statyczna i szybka, ale jest automatycznie odświeżana w tle po upływie określonego czasu. Konfiguruje się ją dodając opcję revalidate w Pages Router lub next.revalidate w App Router.

Odpowiedź w 2 minuty: Incremental Static Regeneration to przełomowa funkcja Next.js, która rozwiązuje dylemat między statycznymi stronami (szybkie, ale nieaktualne) a dynamicznymi (aktualne, ale wolniejsze). ISR umożliwia tworzenie lub aktualizowanie statycznych stron po deploymencie, co oznacza, że możesz mieć miliony stron statycznych bez konieczności rebuild całej aplikacji.

Mechanizm działania ISR opiera się na dwóch koncepcjach: stale-while-revalidate i on-demand revalidation. W pierwszym podejściu określasz czas (w sekundach), po którym strona powinna zostać ponownie wygenerowana. Gdy użytkownik odwiedza stronę po tym czasie, otrzymuje starą wersję (stale), ale w tle generowana jest nowa (revalidate). Kolejni użytkownicy otrzymają już zaktualizowaną wersję.

W App Router ISR konfigurujemy za pomocą opcji revalidate w fetch lub na poziomie segmentu route. W Pages Router używamy revalidate w zwracanym obiekcie z getStaticProps. Dodatkowo Next.js 13+ wprowadził on-demand revalidation, która pozwala ręcznie wyzwolić regenerację konkretnej strony przez API route.

ISR jest idealny dla: e-commerce (produkty aktualizowane regularnie), blogów (nowe posty), dashboardów (dane biznesowe), dokumentacji (rzadkie zmiany, ale duża liczba stron).

Przykład kodu:

// App Router - ISR w Next.js 13+

// 1. ISR z time-based revalidation
async function ProductPage({ params }) {
  // Rewalidacja co 60 sekund
  const product = await fetch(
    `https://api.example.com/products/${params.id}`,
    { next: { revalidate: 60 } }
  ).then(res => res.json());

  return (
    <div>
      <h1>{product.name}</h1>
      <p>Cena: {product.price} PLN</p>
      <p>Ostatnia aktualizacja: {new Date().toLocaleString()}</p>
    </div>
  );
}

// 2. ISR na poziomie segmentu route
export const revalidate = 3600; // Rewalidacja co godzinę dla całego route

async function BlogPost({ params }) {
  const post = await fetch(`https://api.example.com/posts/${params.slug}`)
    .then(res => res.json());

  return <article>{post.content}</article>;
}

// 3. On-demand revalidation - API Route
// app/api/revalidate/route.js
import { revalidatePath, revalidateTag } from 'next/cache';
import { NextResponse } from 'next/server';

export async function POST(request) {
  const { path, tag, secret } = await request.json();

  // Weryfikacja tokenu bezpieczeństwa
  if (secret !== process.env.REVALIDATION_SECRET) {
    return NextResponse.json({ message: 'Invalid token' }, { status: 401 });
  }

  try {
    if (path) {
      // Rewalidacja konkretnej ścieżki
      revalidatePath(path);
      return NextResponse.json({ revalidated: true, path });
    }

    if (tag) {
      // Rewalidacja wszystkich stron z danym tagiem
      revalidateTag(tag);
      return NextResponse.json({ revalidated: true, tag });
    }
  } catch (err) {
    return NextResponse.json({ message: 'Error revalidating' }, { status: 500 });
  }
}

// Użycie tagów w fetch
async function ProductsList() {
  const products = await fetch('https://api.example.com/products', {
    next: {
      revalidate: 3600,
      tags: ['products'] // Tag do on-demand revalidation
    }
  }).then(res => res.json());

  return <div>{products.map(p => <ProductCard key={p.id} {...p} />)}</div>;
}
// Pages Router - klasyczne ISR

// pages/products/[id].js
export async function getStaticProps({ params }) {
  const product = await fetch(`https://api.example.com/products/${params.id}`)
    .then(res => res.json());

  return {
    props: { product },
    revalidate: 60, // Rewalidacja co 60 sekund
  };
}

export async function getStaticPaths() {
  // Generuj tylko najpopularniejsze produkty przy build
  const products = await fetch('https://api.example.com/products/popular')
    .then(res => res.json());

  const paths = products.map(p => ({
    params: { id: p.id.toString() }
  }));

  return {
    paths,
    fallback: 'blocking', // Generuj pozostałe strony on-demand
  };
}

function ProductPage({ product }) {
  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
    </div>
  );
}

export default ProductPage;

// On-demand revalidation w Pages Router
// pages/api/revalidate.js
export default async function handler(req, res) {
  if (req.query.secret !== process.env.REVALIDATION_SECRET) {
    return res.status(401).json({ message: 'Invalid token' });
  }

  try {
    // Rewalidacja konkretnej ścieżki
    await res.revalidate('/products/123');
    return res.json({ revalidated: true });
  } catch (err) {
    return res.status(500).send('Error revalidating');
  }
}
// Webhook do automatycznej rewalidacji (np. z CMS)
// app/api/webhook/route.js
import { revalidateTag } from 'next/cache';

export async function POST(request) {
  const payload = await request.json();

  // Gdy produkt jest aktualizowany w CMS
  if (payload.event === 'product.updated') {
    revalidateTag('products');
    revalidatePath(`/products/${payload.data.id}`);
  }

  // Gdy nowy post na blogu
  if (payload.event === 'post.published') {
    revalidateTag('blog');
    revalidatePath('/blog');
  }

  return Response.json({ success: true });
}

Materiały

15. Jak działa streaming i Suspense w Next.js?

Odpowiedź w 30 sekund: Streaming w Next.js pozwala wysyłać HTML do przeglądarki w częściach, zamiast czekać na załadowanie wszystkich danych. Używając React Suspense, możemy pokazać fallback (np. loading spinner) dla wolnych komponentów, podczas gdy reszta strony renderuje się natychmiast. W App Router streaming jest domyślnie włączony, a implementujemy go przez loading.js lub komponent <Suspense>.

Odpowiedź w 2 minuty: Streaming to technika renderowania, która radykalnie poprawia odczuwaną wydajność aplikacji. Zamiast czekać, aż cała strona zostanie przygotowana na serwerze (co może trwać długo przy wolnych zapytaniach do API/bazy danych), Next.js wysyła HTML do przeglądarki stopniowo. Użytkownik widzi natychmiastowo podstawową strukturę strony i szybko ładujące się sekcje, podczas gdy wolniejsze komponenty "streamują" się w miarę gotowości.

React Suspense jest kluczowym elementem tej architektury. Pozwala "zawiesić" renderowanie komponentu, który czeka na dane (np. asynchroniczne fetch), i pokazać fallback UI. W Next.js App Router mamy trzy sposoby wykorzystania Suspense: loading.js (automatyczny Suspense boundary dla całego route), ręczne Suspense boundaries (owrapowanie wybranych komponentów), oraz streaming całego route (przez loading.tsx).

Streaming działa na poziomie HTTP przez HTTP Streaming (Transfer-Encoding: chunked). Server wysyła kolejne fragmenty HTML, a przeglądarka renderuje je progresywnie. To szczególnie efektywne przy Server Components, które renderują się na serwerze i streamują do klienta.

Korzyści ze streamingu: lepszy Time to First Byte (TTFB), szybszy First Contentful Paint (FCP), lepsza percepcja wydajności (użytkownik widzi progress, nie pustą stronę), możliwość priorytetyzacji ważnych treści.

Przykład kodu:

// App Router - Streaming z Suspense

// 1. loading.js - automatyczny Suspense dla całego route
// app/dashboard/loading.js
export default function Loading() {
  return (
    <div className="animate-pulse">
      <div className="h-8 bg-gray-200 rounded w-1/4 mb-4"></div>
      <div className="h-64 bg-gray-200 rounded"></div>
    </div>
  );
}

// app/dashboard/page.js
async function Dashboard() {
  // Ten komponent będzie streamowany
  const data = await fetch('https://api.example.com/dashboard', {
    cache: 'no-store'
  }).then(res => res.json());

  return <div>{data.content}</div>;
}

export default Dashboard;
// 2. Selektywny Streaming z ręcznymi Suspense boundaries
import { Suspense } from 'react';

// Wolny komponent - async Server Component
async function UserStats({ userId }) {
  // Symulacja wolnego zapytania
  await new Promise(resolve => setTimeout(resolve, 3000));
  const stats = await fetch(`https://api.example.com/users/${userId}/stats`)
    .then(res => res.json());

  return (
    <div className="stats">
      <h3>Statystyki użytkownika</h3>
      <p>Posty: {stats.posts}</p>
      <p>Komentarze: {stats.comments}</p>
    </div>
  );
}

// Szybki komponent
async function UserProfile({ userId }) {
  const user = await fetch(`https://api.example.com/users/${userId}`)
    .then(res => res.json());

  return (
    <div>
      <h2>{user.name}</h2>
      <p>{user.bio}</p>
    </div>
  );
}

// Loading skeleton
function StatsSkeleton() {
  return (
    <div className="animate-pulse">
      <div className="h-6 bg-gray-200 rounded mb-2"></div>
      <div className="h-4 bg-gray-200 rounded w-3/4 mb-2"></div>
      <div className="h-4 bg-gray-200 rounded w-1/2"></div>
    </div>
  );
}

// Główna strona - komponenty streamują się niezależnie
export default function UserPage({ params }) {
  return (
    <div>
      {/* Szybki komponent renderuje się od razu */}
      <Suspense fallback={<div>Ładowanie profilu...</div>}>
        <UserProfile userId={params.id} />
      </Suspense>

      {/* Wolny komponent streamuje się później */}
      <Suspense fallback={<StatsSkeleton />}>
        <UserStats userId={params.id} />
      </Suspense>

      {/* Możemy mieć wiele niezależnych Suspense boundaries */}
      <Suspense fallback={<div>Ładowanie aktywności...</div>}>
        <UserActivity userId={params.id} />
      </Suspense>
    </div>
  );
}
// 3. Nested Suspense - zagnieżdżone granice
async function Comments({ postId }) {
  const comments = await fetch(`https://api.example.com/posts/${postId}/comments`)
    .then(res => res.json());

  return (
    <div>
      {comments.map(comment => (
        <div key={comment.id}>
          <p>{comment.text}</p>
          {/* Zagnieżdżony Suspense dla odpowiedzi */}
          <Suspense fallback={<div>Ładowanie odpowiedzi...</div>}>
            <Replies commentId={comment.id} />
          </Suspense>
        </div>
      ))}
    </div>
  );
}

async function Replies({ commentId }) {
  const replies = await fetch(`https://api.example.com/comments/${commentId}/replies`)
    .then(res => res.json());

  return replies.map(reply => <div key={reply.id}>{reply.text}</div>);
}

export default function PostPage({ params }) {
  return (
    <article>
      <Suspense fallback={<PostSkeleton />}>
        <Post postId={params.id} />
      </Suspense>

      <Suspense fallback={<CommentsSkeleton />}>
        <Comments postId={params.id} />
      </Suspense>
    </article>
  );
}
// 4. Streaming z priorytetyzacją
export default function ProductPage({ params }) {
  return (
    <div>
      {/* Krytyczna treść - bez Suspense, renderuje się jako pierwsza */}
      <ProductHero productId={params.id} />

      {/* Ważna treść - wysokie priorytety */}
      <Suspense fallback={<PriceSkeleton />} priority="high">
        <ProductPrice productId={params.id} />
      </Suspense>

      {/* Średni priorytet */}
      <Suspense fallback={<DescriptionSkeleton />}>
        <ProductDescription productId={params.id} />
      </Suspense>

      {/* Niski priorytet - może ładować się później */}
      <Suspense fallback={<ReviewsSkeleton />}>
        <ProductReviews productId={params.id} />
      </Suspense>

      {/* Najniższy priorytet */}
      <Suspense fallback={<RelatedSkeleton />}>
        <RelatedProducts productId={params.id} />
      </Suspense>
    </div>
  );
}
sequenceDiagram
    participant Browser
    participant Server
    participant Database

    Browser->>Server: GET /dashboard
    Server->>Browser: HTML Shell (instant)
    Note over Browser: Pokazuje layout + loading states

    par Parallel Data Fetching
        Server->>Database: Fetch UserProfile
        Server->>Database: Fetch UserStats
        Server->>Database: Fetch UserActivity
    end

    Database-->>Server: UserProfile data (fast)
    Server->>Browser: Stream UserProfile HTML
    Note over Browser: Zastępuje fallback prawdziwymi danymi

    Database-->>Server: UserStats data (medium)
    Server->>Browser: Stream UserStats HTML
    Note over Browser: Aktualizuje kolejną sekcję

    Database-->>Server: UserActivity data (slow)
    Server->>Browser: Stream UserActivity HTML
    Note over Browser: Wszystko załadowane

Materiały

16. Kiedy wybrać Static Generation a kiedy Server-side Rendering?

Odpowiedź w 30 sekund: Wybierz Static Generation (SSG/ISR) gdy: treść jest taka sama dla wszystkich użytkowników, strona może być pre-renderowana przed requestem, i wydajność jest priorytetem (blogi, landing pages, dokumentacja). Wybierz Server-Side Rendering (SSR) gdy: treść jest spersonalizowana, dane zmieniają się na każde żądanie, lub potrzebujesz nagłówków requestu (dashboardy, strony użytkownika, real-time data).

Odpowiedź w 2 minuty: Wybór między SSG a SSR to jedna z najważniejszych decyzji architektonicznych w Next.js. Kluczowe pytanie brzmi: "Czy mogę wygenerować tę stronę przed requestem użytkownika?"

Wybierz Static Generation (SSG) gdy:

  • Treść jest identyczna dla wszystkich użytkowników
  • Dane pochodzą z Headless CMS, plików Markdown, lub API z rzadkimi zmianami
  • Strona może być cache'owana przez CDN i serwowana błyskawicznie
  • Wydajność i SEO są krytyczne
  • Przykłady: blog, dokumentacja, marketing pages, e-commerce product pages

Używaj ISR (ulepszona SSG) gdy:

  • Treść zmienia się regularnie, ale nie na każde żądanie
  • Możesz zaakceptować "stale" dane przez krótki czas
  • Masz dużą liczbę stron (tysiące produktów, artykułów)
  • Chcesz uniknąć długich build times
  • Przykłady: katalogi produktów, newsy, dane giełdowe (z opóźnieniem)

Wybierz Server-Side Rendering (SSR) gdy:

  • Treść jest spersonalizowana dla użytkownika
  • Potrzebujesz dostępu do request headers (cookies, auth tokens)
  • Dane muszą być zawsze aktualne (real-time)
  • Strona zawiera wrażliwe informacje użytkownika
  • Przykłady: user dashboards, admin panels, personalized feeds, checkout pages

Hybrydowe podejście: Najlepsze aplikacje często łączą obie strategie - SSG dla publicznych stron + CSR dla interaktywności + SSR dla spersonalizowanych sekcji. W App Router można mieszać strategie na poziomie komponentów.

Przykład kodu:

// Tabela decyzyjna - praktyczne przykłady

// ❌ ZŁY WYBÓR: SSR dla bloga
// app/blog/[slug]/page.js
export const dynamic = 'force-dynamic'; // Niepotrzebne!

async function BlogPost({ params }) {
  const post = await fetch(`https://cms.example.com/posts/${params.slug}`, {
    cache: 'no-store' // Blog nie potrzebuje SSR!
  }).then(res => res.json());

  return <article>{post.content}</article>;
}

// ✅ DOBRY WYBÓR: ISR dla bloga
async function BlogPost({ params }) {
  const post = await fetch(`https://cms.example.com/posts/${params.slug}`, {
    next: { revalidate: 3600 } // Aktualizacja co godzinę
  }).then(res => res.json());

  return <article>{post.content}</article>;
}
// ❌ ZŁY WYBÓR: SSG dla dashboardu użytkownika
async function UserDashboard({ params }) {
  // To nie zadziała poprawnie - dane będą z build time!
  const userData = await fetch(`https://api.example.com/users/${params.id}`)
    .then(res => res.json());

  return <div>Witaj, {userData.name}</div>;
}

// ✅ DOBRY WYBÓR: SSR dla dashboardu
import { cookies } from 'next/headers';

async function UserDashboard() {
  const cookieStore = cookies();
  const token = cookieStore.get('auth-token');

  const userData = await fetch('https://api.example.com/user/me', {
    headers: { Authorization: `Bearer ${token.value}` },
    cache: 'no-store' // Zawsze świeże dane
  }).then(res => res.json());

  return <div>Witaj, {userData.name}</div>;
}
// ✅ NAJLEPSZE: Hybrydowe podejście
// app/products/[id]/page.js

// Statyczna część - informacje o produkcie
async function ProductInfo({ productId }) {
  const product = await fetch(`https://api.example.com/products/${productId}`, {
    next: { revalidate: 3600 } // ISR - aktualizacja co godzinę
  }).then(res => res.json());

  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
      <img src={product.image} alt={product.name} />
    </div>
  );
}

// Dynamiczna część - inwentarz (SSR)
async function ProductInventory({ productId }) {
  const inventory = await fetch(
    `https://api.example.com/products/${productId}/inventory`,
    { cache: 'no-store' } // SSR - zawsze aktualne
  ).then(res => res.json());

  return (
    <div>
      <p className={inventory.inStock ? 'text-green-600' : 'text-red-600'}>
        {inventory.inStock ? 'W magazynie' : 'Brak w magazynie'}
      </p>
      <p>Dostępne sztuki: {inventory.quantity}</p>
    </div>
  );
}

// Interaktywna część - koszyk (CSR)
'use client';
import { useState } from 'react';

function AddToCart({ productId }) {
  const [quantity, setQuantity] = useState(1);

  const handleAddToCart = async () => {
    await fetch('/api/cart', {
      method: 'POST',
      body: JSON.stringify({ productId, quantity })
    });
  };

  return (
    <div>
      <input
        type="number"
        value={quantity}
        onChange={e => setQuantity(e.target.value)}
      />
      <button onClick={handleAddToCart}>Dodaj do koszyka</button>
    </div>
  );
}

// Główny komponent łączący wszystkie strategie
import { Suspense } from 'react';

export default function ProductPage({ params }) {
  return (
    <div>
      {/* ISR - statyczna treść produktu */}
      <ProductInfo productId={params.id} />

      {/* SSR - dynamiczny stan magazynu z Suspense */}
      <Suspense fallback={<InventorySkeleton />}>
        <ProductInventory productId={params.id} />
      </Suspense>

      {/* CSR - interaktywny koszyk */}
      <AddToCart productId={params.id} />

      {/* ISR - recenzje */}
      <Suspense fallback={<ReviewsSkeleton />}>
        <ProductReviews productId={params.id} />
      </Suspense>
    </div>
  );
}
// Decyzyjny flowchart w kodzie
function chooseRenderingStrategy(page) {
  // Czy treść jest spersonalizowana?
  if (page.requiresAuth || page.usesUserData) {
    return 'SSR';
  }

  // Czy dane muszą być real-time?
  if (page.realTimeData) {
    return 'SSR';
  }

  // Czy treść się zmienia?
  if (page.contentChangesFrequently) {
    // Jak często?
    if (page.changesEverySecond) {
      return 'SSR + CSR'; // SSR initial + client updates
    } else if (page.changesEveryMinute || page.changesHourly) {
      return 'ISR'; // Incremental Static Regeneration
    }
  }

  // Domyślnie: Static Generation
  return 'SSG';
}

// Przykłady zastosowań:
const examples = {
  '/blog/[slug]': 'SSG/ISR', // Treść publiczna, rzadko zmieniana
  '/products/[id]': 'ISR', // Katalog, regularne aktualizacje
  '/dashboard': 'SSR', // Spersonalizowane, wymaga auth
  '/admin': 'SSR', // Real-time, wrażliwe dane
  '/': 'SSG', // Landing page, statyczna treść
  '/about': 'SSG', // O nas, bardzo rzadko zmieniana
  '/search': 'SSR', // Wyniki wyszukiwania, dynamiczne
  '/api/products': 'Edge Runtime', // API, global distribution
};
flowchart TD
    A[Nowa strona w Next.js] --> B{Treść spersonalizowana<br/>dla użytkownika?}
    B -->|Tak| C[SSR]
    B -->|Nie| D{Dane muszą być<br/>real-time?}
    D -->|Tak| C
    D -->|Nie| E{Treść się zmienia?}
    E -->|Nigdy/Rzadko| F[SSG]
    E -->|Regularnie| G{Jak często?}
    G -->|Co sekundę/minutę| C
    G -->|Co godzinę/dzień| H[ISR]
    E -->|Częściowo| I[Hybrydowe:<br/>SSG + CSR]

    F --> J[✓ Najlepsza wydajność<br/>✓ Świetne SEO<br/>✓ Tanie hosting]
    H --> K[✓ Dobra wydajność<br/>✓ Aktualne dane<br/>✓ Szybkie buildy]
    C --> L[✓ Zawsze aktualne<br/>✓ Personalizacja<br/>⚠ Wolniejsze]
    I --> M[✓ Najlepsze UX<br/>✓ Balans wydajności]

Materiały

17. Czym jest Partial Prerendering (PPR) w Next.js 14+?

Odpowiedź w 30 sekund: Partial Prerendering (PPR) to eksperymentalna funkcja w Next.js 14+, która łączy statyczne i dynamiczne renderowanie w jednym route. Statyczne części strony są pre-renderowane i wysyłane natychmiast, podczas gdy dynamiczne sekcje (owrapowane w Suspense) streamują się później. To daje najlepsze z obu światów: błyskawiczny First Paint ze statycznych części + świeże dane w dynamicznych sekcjach.

Odpowiedź w 2 minuty: Partial Prerendering to rewolucyjna funkcja wprowadzona w Next.js 14, która zmienia sposób myślenia o renderowaniu. Tradycyjnie musieliśmy wybierać: cały route jest statyczny (SSG) lub całkowicie dynamiczny (SSR). PPR pozwala mieć jedno i drugie - statyczny "shell" strony z dynamicznymi "dziurami" dla treści wymagającej SSR.

Mechanizm działania: podczas build time Next.js pre-renderuje wszystkie części strony, które mogą być statyczne. Komponenty owrapowane w <Suspense> są oznaczane jako "dynamiczne holes" i zastępowane placeholder-ami. Gdy użytkownik wysyła request, otrzymuje natychmiast statyczny HTML shell (z CDN), a dynamiczne sekcje streamują się z serwera. To działa automatycznie - wystarczy użyć Suspense, a Next.js rozpoznaje granice między static/dynamic.

Kluczowe zalety PPR:

  • Instant Page Load: Statyczny shell ładuje się błyskawicznie z CDN
  • Fresh Data: Dynamiczne sekcje zawsze aktualne
  • Lepsze SEO: Crawlery widzą pre-renderowany content
  • Automatyczna optymalizacja: Next.js sam decyduje co statyczne, co dynamiczne
  • Granularna kontrola: Suspense boundaries definiują co streamuje się później

PPR działa najlepiej dla: e-commerce (statyczna treść produktu + dynamiczny inventory), dashboardów (statyczny layout + dynamiczne dane użytkownika), personalizowanych stron (statyczna struktura + dynamiczne rekomendacje).

Przykład kodu:

// next.config.js - włączenie PPR (eksperymentalne w Next.js 14-15)
module.exports = {
  experimental: {
    ppr: true, // Włącz Partial Prerendering globalnie
  },
};

// Lub per-route:
// app/products/[id]/page.js
export const experimental_ppr = true;
// Przykład 1: E-commerce Product Page z PPR
import { Suspense } from 'react';

// Statyczny komponent - pre-renderowany
async function ProductDetails({ productId }) {
  const product = await fetch(`https://api.example.com/products/${productId}`, {
    // Bez cache options - domyślnie cache'owane (statyczne)
  }).then(res => res.json());

  return (
    <div>
      <h1>{product.name}</h1>
      <img src={product.image} alt={product.name} />
      <p>{product.description}</p>
      <p className="text-2xl font-bold">{product.price} PLN</p>
    </div>
  );
}

// Dynamiczny komponent - streamowany
async function ProductAvailability({ productId }) {
  const availability = await fetch(
    `https://api.example.com/products/${productId}/availability`,
    { cache: 'no-store' } // Dynamiczne - zawsze świeże
  ).then(res => res.json());

  return (
    <div className="border-t pt-4 mt-4">
      <p className={availability.inStock ? 'text-green-600' : 'text-red-600'}>
        {availability.inStock
          ? `✓ W magazynie: ${availability.quantity} szt.`
          : '✗ Brak w magazynie'}
      </p>
      <p className="text-sm text-gray-600">
        Przewidywana dostawa: {availability.estimatedDelivery}
      </p>
    </div>
  );
}

// Dynamiczne recenzje
async function ProductReviews({ productId }) {
  const reviews = await fetch(
    `https://api.example.com/products/${productId}/reviews?latest=5`,
    { cache: 'no-store', next: { tags: ['reviews'] } }
  ).then(res => res.json());

  return (
    <div className="mt-8">
      <h2 className="text-xl font-bold mb-4">Ostatnie recenzje</h2>
      {reviews.map(review => (
        <div key={review.id} className="mb-4 p-4 bg-gray-50 rounded">
          <div className="flex items-center mb-2">
            <span className="font-semibold">{review.author}</span>
            <span className="ml-2 text-yellow-500">{'★'.repeat(review.rating)}</span>
          </div>
          <p>{review.comment}</p>
        </div>
      ))}
    </div>
  );
}

// Główna strona - PPR w akcji
export default async function ProductPage({ params }) {
  return (
    <div className="max-w-4xl mx-auto p-8">
      {/* STATYCZNA część - pre-renderowana przy build */}
      <ProductDetails productId={params.id} />

      {/* DYNAMICZNE części - streamowane przy request */}
      <Suspense fallback={
        <div className="animate-pulse">
          <div className="h-16 bg-gray-200 rounded"></div>
        </div>
      }>
        <ProductAvailability productId={params.id} />
      </Suspense>

      <Suspense fallback={
        <div className="animate-pulse mt-8">
          <div className="h-6 bg-gray-200 rounded w-1/4 mb-4"></div>
          <div className="h-24 bg-gray-200 rounded mb-2"></div>
          <div className="h-24 bg-gray-200 rounded"></div>
        </div>
      }>
        <ProductReviews productId={params.id} />
      </Suspense>

      {/* Nawet dodanie do koszyka może być osobnym boundary */}
      <Suspense fallback={<button disabled>Ładowanie...</button>}>
        <AddToCartButton productId={params.id} />
      </Suspense>
    </div>
  );
}
// Przykład 2: Dashboard z PPR
import { Suspense } from 'react';
import { cookies } from 'next/headers';

// Statyczny layout i nawigacja
function DashboardShell({ children }) {
  return (
    <div className="min-h-screen">
      <nav className="bg-blue-600 text-white p-4">
        <h1 className="text-2xl font-bold">Dashboard</h1>
        <ul className="flex gap-4 mt-2">
          <li><a href="/dashboard">Przegląd</a></li>
          <li><a href="/dashboard/stats">Statystyki</a></li>
          <li><a href="/dashboard/settings">Ustawienia</a></li>
        </ul>
      </nav>
      <main className="p-8">
        {children}
      </main>
    </div>
  );
}

// Dynamiczne dane użytkownika
async function UserGreeting() {
  const cookieStore = cookies();
  const token = cookieStore.get('auth');

  const user = await fetch('https://api.example.com/user/me', {
    headers: { Authorization: `Bearer ${token?.value}` },
    cache: 'no-store'
  }).then(res => res.json());

  return <h2 className="text-2xl mb-4">Witaj, {user.name}!</h2>;
}

// Dynamiczne statystyki
async function DashboardStats() {
  const stats = await fetch('https://api.example.com/stats/today', {
    cache: 'no-store'
  }).then(res => res.json());

  return (
    <div className="grid grid-cols-3 gap-4">
      <div className="bg-white p-6 rounded shadow">
        <h3 className="text-gray-600">Sprzedaż dziś</h3>
        <p className="text-3xl font-bold">{stats.sales} PLN</p>
      </div>
      <div className="bg-white p-6 rounded shadow">
        <h3 className="text-gray-600">Nowi użytkownicy</h3>
        <p className="text-3xl font-bold">{stats.newUsers}</p>
      </div>
      <div className="bg-white p-6 rounded shadow">
        <h3 className="text-gray-600">Zamówienia</h3>
        <p className="text-3xl font-bold">{stats.orders}</p>
      </div>
    </div>
  );
}

export const experimental_ppr = true;

export default function DashboardPage() {
  return (
    <DashboardShell>
      {/* Dynamiczne powitanie */}
      <Suspense fallback={<div className="h-8 bg-gray-200 rounded w-1/4 mb-4"></div>}>
        <UserGreeting />
      </Suspense>

      {/* Dynamiczne statystyki */}
      <Suspense fallback={
        <div className="grid grid-cols-3 gap-4 animate-pulse">
          <div className="h-32 bg-gray-200 rounded"></div>
          <div className="h-32 bg-gray-200 rounded"></div>
          <div className="h-32 bg-gray-200 rounded"></div>
        </div>
      }>
        <DashboardStats />
      </Suspense>
    </DashboardShell>
  );
}
// Przykład 3: Kontrola nad PPR - opt-in/opt-out
// app/blog/[slug]/page.js

// Wyłączenie PPR dla konkretnego route
export const experimental_ppr = false;

// Lub wymuszenie całkowicie statycznego renderowania
export const dynamic = 'force-static';

// Lub wymuszenie dynamicznego
export const dynamic = 'force-dynamic';

// Granularna kontrola przez Suspense + dynamic functions
import { cookies, headers } from 'next/headers';

async function ViewCount({ slug }) {
  // Użycie dynamic function sprawia, że komponent jest dynamiczny
  const cookieStore = cookies();
  const viewId = cookieStore.get('view-id');

  const count = await fetch(`https://api.example.com/views/${slug}`, {
    cache: 'no-store'
  }).then(res => res.json());

  return <span>{count.views} wyświetleń</span>;
}

export default async function BlogPost({ params }) {
  // Ten fetch jest cache'owany - część statyczna
  const post = await fetch(`https://cms.example.com/posts/${params.slug}`)
    .then(res => res.json());

  return (
    <article>
      <h1>{post.title}</h1>

      {/* Statyczna treść artykułu */}
      <div dangerouslySetInnerHTML={{ __html: post.content }} />

      {/* Dynamiczny licznik wyświetleń */}
      <Suspense fallback={<span>...</span>}>
        <ViewCount slug={params.slug} />
      </Suspense>
    </article>
  );
}
graph TB
    subgraph "Build Time"
        A[Next.js Build] --> B[Analiza komponentów]
        B --> C{Czy używa<br/>dynamic functions?}
        C -->|Nie| D[Pre-render HTML]
        C -->|Tak + Suspense| E[Utwórz Static Shell<br/>+ Dynamic Hole]
        C -->|Tak bez Suspense| F[Oznacz jako<br/>fully dynamic]
    end

    subgraph "Request Time"
        G[User Request] --> H{PPR enabled?}
        H -->|Tak| I[Wyślij Static Shell<br/>z CDN instant]
        H -->|Nie| J[Full SSR]
        I --> K[Stream Dynamic Parts]
        K --> L[Pełna strona]
        J --> L
    end

    D --> M[(Static HTML)]
    E --> N[(Static Shell)]
    F --> O[(Dynamic Route)]

    M --> I
    N --> I
    O --> J

    style D fill:#90EE90
    style E fill:#FFD700
    style F fill:#FF6B6B

Materiały

Pobieranie Danych w Next.js

Jak działa fetch w Server Components i czym różni się od fetch po stronie klienta?

Odpowiedź w 30 sekund: Next.js rozszerza natywne API fetch w Server Components, dodając automatyczne cachowanie, deduplikację zapytań i rewalidację danych. W przeciwieństwie do fetch po stronie klienta, fetch w Server Components wykonuje się na serwerze podczas renderowania, nie obciąża przeglądarki użytkownika i pozwala na bezpieczne używanie kluczy API bez ich eksponowania.

Odpowiedź w 2 minuty: Next.js wprowadza rozszerzenia do natywnego fetch API, które działają tylko w Server Components. Kluczową różnicą jest to, że fetch wykonuje się na serwerze podczas budowania strony lub obsługi żądania SSR, a nie w przeglądarce użytkownika. Next.js automatycznie cachuje wyniki zapytań fetch, co oznacza, że identyczne zapytania w różnych komponentach są deduplikowane i wykonywane tylko raz.

Server Components mogą bezpiecznie wykonywać zapytania do baz danych lub API używając kluczy tajnych, ponieważ kod nigdy nie trafia do przeglądarki. Fetch w Server Components wspiera również konfigurację zachowania cache poprzez opcje cache i next.revalidate, co pozwala na precyzyjną kontrolę nad tym, jak długo dane są przechowywane i kiedy są odświeżane.

W przypadku fetch po stronie klienta (w Client Components), zapytania wykonują się w przeglądarce użytkownika, nie są automatycznie cachowane przez Next.js i wymagają używania bibliotek jak SWR lub React Query do zarządzania stanem i cache. Fetch po stronie klienta jest odpowiedni dla danych dynamicznych, które zmieniają się w odpowiedzi na interakcje użytkownika.

Dodatkowo, fetch w Server Components nie powoduje efektu "waterfall" jeśli są używane równolegle - Next.js automatycznie optymalizuje równoległe zapytania, podczas gdy po stronie klienta wymaga to świadomego użycia Promise.all() lub podobnych konstrukcji.

Przykład kodu:

// Server Component - fetch z automatycznym cachowaniem
async function ProductList() {
  // Zapytanie jest cachowane domyślnie
  const res = await fetch('https://api.example.com/products', {
    cache: 'force-cache', // Domyślne zachowanie - cachuj na zawsze
  });
  const products = await res.json();

  return (
    <div>
      {products.map(product => (
        <ProductCard key={product.id} product={product} />
      ))}
    </div>
  );
}

// Server Component - fetch z rewalidacją co 60 sekund
async function LivePrices() {
  const res = await fetch('https://api.example.com/prices', {
    next: { revalidate: 60 }, // Rewaliduj co 60 sekund
  });
  const prices = await res.json();

  return <PriceDisplay prices={prices} />;
}

// Server Component - bez cachowania (zawsze świeże dane)
async function UserDashboard() {
  const res = await fetch('https://api.example.com/user/stats', {
    cache: 'no-store', // Nie cachuj - zawsze pobieraj świeże dane
  });
  const stats = await res.json();

  return <Dashboard stats={stats} />;
}

// Client Component - tradycyjny fetch po stronie klienta
'use client';

import { useEffect, useState } from 'react';

function ClientDataComponent() {
  const [data, setData] = useState(null);

  useEffect(() => {
    // Wykonuje się w przeglądarce, wymaga ręcznego zarządzania stanem
    fetch('/api/data')
      .then(res => res.json())
      .then(setData);
  }, []);

  if (!data) return <div>Ładowanie...</div>;

  return <div>{JSON.stringify(data)}</div>;
}

Materiały

Jak skonfigurować cache i rewalidację danych w Next.js?

Odpowiedź w 30 sekund: Next.js oferuje kilka mechanizmów kontroli cache: opcja cache w fetch (force-cache, no-store), next.revalidate dla time-based revalidation, revalidatePath() i revalidateTag() dla on-demand revalidation oraz segment config options jak export const revalidate. Możesz kontrolować cache na poziomie pojedynczego zapytania, całego route'a lub używać tagów do grupowego invalidowania cache.

Odpowiedź w 2 minuty: Next.js App Router wprowadza wielopoziomowy system cachowania i rewalidacji danych. Na poziomie pojedynczego zapytania fetch, możesz użyć opcji cache: 'force-cache' (domyślnie) dla statycznego cachowania lub cache: 'no-store' dla dynamicznych danych. Dla time-based revalidation używasz next: { revalidate: seconds }, co sprawia, że dane są odświeżane po określonym czasie.

Na poziomie całego route'a możesz eksportować konfigurację segmentu, np. export const revalidate = 3600 dla rewalidacji co godzinę, lub export const dynamic = 'force-dynamic' aby całkowicie wyłączyć cachowanie. Next.js wspiera również Incremental Static Regeneration (ISR), gdzie strony są budowane statycznie, ale automatycznie regenerowane w tle po upływie określonego czasu.

Dla bardziej zaawansowanej kontroli, Next.js oferuje on-demand revalidation poprzez funkcje revalidatePath() i revalidateTag(). revalidatePath() invaliduje cache dla konkretnej ścieżki URL, podczas gdy revalidateTag() pozwala na grupowe invalidowanie wielu zapytań oznaczonych tym samym tagiem - to szczególnie przydatne w Server Actions po mutacjach danych.

System cache w Next.js działa na wielu poziomach: Request Memoization (deduplikacja w ramach jednego render), Data Cache (persystentny cache HTTP), Full Route Cache (cached HTML i RSC payload) oraz Router Cache (cache po stronie klienta). Rozumienie tych warstw jest kluczowe dla efektywnego zarządzania cachowaniem.

Przykład kodu:

// 1. Konfiguracja cache na poziomie fetch
async function getProducts() {
  // Cachowanie statyczne - dane nie zmieniają się
  const static = await fetch('https://api.example.com/categories', {
    cache: 'force-cache', // Domyślne - cachuj na zawsze
  });

  // Time-based revalidation - odświeżaj co 60 sekund
  const products = await fetch('https://api.example.com/products', {
    next: { revalidate: 60 },
  });

  // Brak cachowania - zawsze świeże dane
  const live = await fetch('https://api.example.com/stock', {
    cache: 'no-store',
  });

  return { static, products, live };
}

// 2. Konfiguracja cache na poziomie route'a (segment config)
// app/products/page.tsx
export const revalidate = 3600; // Rewaliduj co godzinę
export const dynamic = 'auto'; // 'auto' | 'force-dynamic' | 'force-static'
export const dynamicParams = true; // true | false

export default async function ProductsPage() {
  const res = await fetch('https://api.example.com/products');
  const products = await res.json();

  return <ProductList products={products} />;
}

// 3. Używanie tagów dla grupowej rewalidacji
async function getProductWithReviews(id: string) {
  // Oznacz zapytania tagami
  const product = await fetch(`https://api.example.com/products/${id}`, {
    next: {
      tags: ['products', `product-${id}`],
      revalidate: 3600,
    },
  });

  const reviews = await fetch(`https://api.example.com/products/${id}/reviews`, {
    next: {
      tags: ['reviews', `product-${id}`],
      revalidate: 300,
    },
  });

  return { product: await product.json(), reviews: await reviews.json() };
}

// 4. On-demand revalidation w Server Action
'use server';

import { revalidatePath, revalidateTag } from 'next/cache';

export async function updateProduct(formData: FormData) {
  const productId = formData.get('id') as string;

  // Wykonaj aktualizację w bazie danych
  await db.products.update({
    where: { id: productId },
    data: { name: formData.get('name') as string },
  });

  // Invaliduj cache dla konkretnej ścieżki
  revalidatePath(`/products/${productId}`);

  // Lub invaliduj wszystkie zapytania z danym tagiem
  revalidateTag(`product-${productId}`);
  revalidateTag('products'); // Invaliduj listę produktów
}

// 5. Różne strategie dla różnych części aplikacji
// app/layout.tsx - globalna konfiguracja
export const revalidate = false; // Wyłącz automatyczną rewalidację

// app/blog/page.tsx - ISR z rewalidacją
export const revalidate = 86400; // Raz dziennie

async function BlogPage() {
  const posts = await fetch('https://api.example.com/posts', {
    next: { revalidate: 86400 },
  });

  return <PostsList posts={await posts.json()} />;
}

// app/dashboard/page.tsx - zawsze dynamiczne
export const dynamic = 'force-dynamic';
export const revalidate = 0;

async function DashboardPage() {
  const data = await fetch('https://api.example.com/user/data', {
    cache: 'no-store',
  });

  return <Dashboard data={await data.json()} />;
}

Materiały

Czym jest funkcja generateStaticParams i kiedy jej używać?

Odpowiedź w 30 sekund: generateStaticParams to funkcja używana w dynamicznych route'ach, która generuje listę parametrów URL do pre-renderowania podczas build time. Zastępuje getStaticPaths z Pages Router i jest używana do tworzenia statycznych stron dla znanych ścieżek dynamicznych (np. posty bloga, produkty), co znacznie poprawia wydajność przez eliminację renderowania on-demand.

Odpowiedź w 2 minuty: generateStaticParams jest kluczową funkcją w Next.js App Router do implementacji Static Site Generation (SSG) dla dynamicznych route'ów. Podczas procesu budowania, Next.js wywołuje tę funkcję aby uzyskać listę wszystkich możliwych wartości parametrów ścieżki, a następnie pre-renderuje HTML dla każdej kombinacji. To znacznie poprawia wydajność, ponieważ strony są już gotowe i mogą być serwowane z CDN zamiast renderowania on-demand.

Funkcję eksportujemy z pliku page.tsx lub layout.tsx w folderze z dynamicznym segmentem (np. [id] lub [slug]). Zwraca ona tablicę obiektów, gdzie każdy obiekt zawiera wartości parametrów dla jednej strony do wygenerowania. Next.js automatycznie obsługuje zagnieżdżone dynamiczne route'y, pozwalając na eksport generateStaticParams na każdym poziomie hierarchii.

Używaj generateStaticParams gdy masz skończoną, przewidywalną liczbę dynamicznych stron (np. produkty w sklepie, posty na blogu, strony dokumentacji). Dla route'ów z nieskończoną liczbą możliwych wartości lub gdy lista zmienia się bardzo często, lepiej użyć dynamicznego renderowania. Możesz również skonfigurować zachowanie dla nieznanych parametrów poprzez dynamicParams - gdy ustawiony na false, Next.js zwróci 404 dla stron niegenerowanych podczas build time.

generateStaticParams współpracuje z ISR (Incremental Static Regeneration), pozwalając na dodawanie nowych stron po deploymencie lub aktualizację istniejących według harmonogramu rewalidacji. Jest to elastyczne rozwiązanie łączące korzyści statycznego generowania z możliwością aktualizacji treści.

Przykład kodu:

// app/products/[id]/page.tsx
interface Product {
  id: string;
  name: string;
  description: string;
}

// Generuj statyczne parametry podczas build time
export async function generateStaticParams() {
  // Pobierz listę wszystkich produktów
  const products = await fetch('https://api.example.com/products').then(res =>
    res.json()
  );

  // Zwróć tablicę obiektów z parametrami
  return products.map((product: Product) => ({
    id: product.id,
  }));
}

// Strona produktu - renderowana statycznie dla każdego ID
export default async function ProductPage({
  params,
}: {
  params: { id: string };
}) {
  const product = await fetch(
    `https://api.example.com/products/${params.id}`
  ).then(res => res.json());

  return (
    <div>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
    </div>
  );
}

// Opcjonalnie: kontroluj zachowanie dla nieznanych parametrów
export const dynamicParams = true; // true (domyślnie) - generuj on-demand
                                   // false - zwróć 404

// app/blog/[category]/[slug]/page.tsx
// Przykład zagnieżdżonych dynamicznych route'ów
export async function generateStaticParams() {
  const posts = await fetch('https://api.example.com/posts').then(res =>
    res.json()
  );

  // Zwróć wszystkie kombinacje category i slug
  return posts.map((post: any) => ({
    category: post.category,
    slug: post.slug,
  }));
}

export default async function BlogPost({
  params,
}: {
  params: { category: string; slug: string };
}) {
  const post = await fetch(
    `https://api.example.com/posts/${params.category}/${params.slug}`
  ).then(res => res.json());

  return <article>{/* Render post */}</article>;
}

// app/docs/[...slug]/page.tsx
// Catch-all route z generateStaticParams
export async function generateStaticParams() {
  const docs = await getDocsPaths(); // Funkcja zwracająca wszystkie ścieżki dokumentacji

  return docs.map((doc) => ({
    slug: doc.path.split('/'), // ['getting-started', 'installation']
  }));
}

export default async function DocPage({
  params,
}: {
  params: { slug: string[] };
}) {
  const path = params.slug.join('/');
  const doc = await getDocByPath(path);

  return <DocContent doc={doc} />;
}

// Przykład z ISR - generowanie statyczne + rewalidacja
// app/news/[id]/page.tsx
export const revalidate = 3600; // Rewaliduj co godzinę

export async function generateStaticParams() {
  // Generuj tylko najpopularniejsze artykuły podczas build time
  const topArticles = await fetch(
    'https://api.example.com/news/top?limit=100'
  ).then(res => res.json());

  return topArticles.map((article: any) => ({
    id: article.id,
  }));
}

// dynamicParams = true pozwala na generowanie nowych stron on-demand
export const dynamicParams = true;

export default async function NewsArticle({
  params,
}: {
  params: { id: string };
}) {
  const article = await fetch(
    `https://api.example.com/news/${params.id}`
  ).then(res => res.json());

  return <ArticleView article={article} />;
}

// Przykład z danymi z bazy danych
// app/users/[username]/page.tsx
import { db } from '@/lib/db';

export async function generateStaticParams() {
  // Pobierz usernames z bazy danych
  const users = await db.user.findMany({
    select: { username: true },
    take: 1000, // Limit dla bardzo dużych zbiorów
  });

  return users.map((user) => ({
    username: user.username,
  }));
}

export default async function UserProfile({
  params,
}: {
  params: { username: string };
}) {
  const user = await db.user.findUnique({
    where: { username: params.username },
  });

  if (!user) {
    notFound(); // Użyj Next.js notFound() dla 404
  }

  return <ProfileView user={user} />;
}

Materiały

21. Jak obsługiwać równoległe zapytania o dane w Server Components?

Odpowiedź dostępna w pełnej wersji. Odblokuj 20 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
22. Czym są Server Actions i jak ich używać do mutacji danych?

Odpowiedź dostępna w pełnej wersji. Odblokuj 20 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi

Next.js - Optymalizacja

23. Jak działa komponent next/image i jakie oferuje optymalizacje?

Odpowiedź dostępna w pełnej wersji. Odblokuj 20 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
24. Czym jest next/font i jakie daje korzyści wydajnościowe?

Odpowiedź dostępna w pełnej wersji. Odblokuj 20 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
25. Jak zoptymalizować ładowanie skryptów zewnętrznych w Next.js?

Odpowiedź dostępna w pełnej wersji. Odblokuj 20 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
26. Jakie techniki optymalizacji bundle oferuje Next.js?

Odpowiedź dostępna w pełnej wersji. Odblokuj 20 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
27. Jak skonfigurować lazy loading komponentów w Next.js?

Odpowiedź dostępna w pełnej wersji. Odblokuj 20 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi

Metadata i SEO w Next.js

28. 28. Jak definiować metadata statyczne i dynamiczne w App Router?

Odpowiedź dostępna w pełnej wersji. Odblokuj 20 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
29. 29. Czym jest plik opengraph-image i jak go używać?

Odpowiedź dostępna w pełnej wersji. Odblokuj 20 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
30. 30. Jak wygenerować sitemap.xml i robots.txt w Next.js?

Odpowiedź dostępna w pełnej wersji. Odblokuj 20 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
31. 31. Jak zaimplementować canonical URLs i hreflang w Next.js?

Odpowiedź dostępna w pełnej wersji. Odblokuj 20 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi

Next.js - API i Backend

32. Jak tworzyć Route Handlers (API routes) w App Router?

Odpowiedź dostępna w pełnej wersji. Odblokuj 20 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
33. Czym różnią się Route Handlers od Server Actions?

Odpowiedź dostępna w pełnej wersji. Odblokuj 20 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
34. Jak obsługiwać różne metody HTTP w Route Handlers?

Odpowiedź dostępna w pełnej wersji. Odblokuj 20 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
35. Jak zaimplementować API z walidacją i obsługą błędów?

Odpowiedź dostępna w pełnej wersji. Odblokuj 20 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi

Next.js - Konfiguracja i Deployment

36. Jakie są kluczowe opcje w pliku next.config.js?

Odpowiedź dostępna w pełnej wersji. Odblokuj 20 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
37. Jak skonfigurować zmienne środowiskowe w Next.js?

Odpowiedź dostępna w pełnej wersji. Odblokuj 20 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
38. Czym różni się deployment na Vercel od self-hosted?

Odpowiedź dostępna w pełnej wersji. Odblokuj 20 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
39. Jak skonfigurować Next.js do pracy z Docker?

Odpowiedź dostępna w pełnej wersji. Odblokuj 20 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi
40. Jak monitorować wydajność aplikacji Next.js w produkcji?

Odpowiedź dostępna w pełnej wersji. Odblokuj 20 pozostałych odpowiedzi i ucz się z pełnego zestawu.

Odblokuj odpowiedzi

Chcesz poznać wszystkie odpowiedzi?

Uzyskaj pełny dostęp do 40 pytań rekrutacyjnych z Next.js oraz pozostałych technologii.

Zobacz plany cenowe