50+ React Server Components Pytania Rekrutacyjne 2026: RSC, Server Actions i Streaming

50+ React Server Components Pytania Rekrutacyjne 2026: RSC, Server Actions i Streaming

React Server Components (RSC) to największa zmiana w React od wprowadzenia hooks. Zmieniają fundamentalnie sposób myślenia o tym gdzie i jak renderować komponenty. Na rozmowach rekrutacyjnych w 2025/2026 znajomość RSC jest obowiązkowa dla React developerów - pytania padają o różnice z Client Components, Server Actions, streaming, i praktyczne patterns.

Spis Treści

  1. Podstawy Server Components Pytania
  2. Data Fetching w Server Components Pytania
  3. Streaming i Suspense Pytania
  4. Server Actions Pytania
  5. Composition Patterns Pytania
  6. Performance i Optymalizacja Pytania
  7. Migracja i Adoption Pytania
  8. Zaawansowane Patterns Pytania
  9. Zadania Praktyczne
  10. Powiązane Artykuły

Odpowiedź w 30 sekund

Czym są React Server Components?

Server Components to komponenty które renderują się wyłącznie na serwerze - nigdy nie trafiają do bundle JavaScript klienta. Mogą bezpośrednio czytać z bazy danych, systemu plików, i API bez tworzenia dodatkowych endpointów. Nie mają dostępu do hooks (useState, useEffect) ani browser APIs. Domyślnie w Next.js App Router wszystkie komponenty są Server Components - dodajesz "use client" gdy potrzebujesz interaktywności.

Odpowiedź w 2 minuty

React Server Components rozwiązują fundamentalny problem tradycyjnych React aplikacji - cały kod komponentów trafia do klienta, nawet jeśli służy tylko do wyświetlenia statycznych danych. W klasycznej architekturze React komponent musi: pobrać dane z API, obsłużyć loading/error states, i renderować UI. Cały ten kod trafia do bundle.

RSC przenoszą rendering na serwer. Server Component wykonuje się na serwerze, ma bezpośredni dostęp do bazy danych i systemu plików, i wysyła do klienta gotowy wynik - HTML plus specjalny format RSC Payload który React używa do aktualizacji DOM. Kod samego komponentu nigdy nie trafia do klienta.

Kluczowa koncepcja to podział na Server i Client Components. Server Components są domyślne - nie mają dostępu do useState, useEffect, event handlers, ani browser APIs. Client Components oznaczasz dyrektywą "use client" na początku pliku - mają pełną interaktywność ale są w bundle klienta.

Ważne: "use client" nie oznacza że komponent renderuje się tylko na kliencie. Client Components nadal są SSR (Server-Side Rendered) dla pierwszego ładowania strony, ale potem hydrowane na kliencie. Różnica polega na tym że Server Components nie są hydrowane - nie ma ich w bundle JavaScript.

Server Actions to funkcje async oznaczone "use server" które wykonują się na serwerze ale mogą być wywoływane z klienta. Idealne do form submissions i mutacji danych - integrują się z formularzami przez prop action.

// Server Component (domyślny)
async function UserProfile({ userId }: { userId: string }) {
  // Bezpośredni dostęp do bazy - wykonuje się na serwerze
  const user = await db.users.findUnique({ where: { id: userId } });

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
      {/* Client Component dla interaktywności */}
      <FollowButton userId={userId} />
    </div>
  );
}

// Client Component
"use client";

function FollowButton({ userId }: { userId: string }) {
  const [isFollowing, setIsFollowing] = useState(false);

  return (
    <button onClick={() => setIsFollowing(!isFollowing)}>
      {isFollowing ? 'Unfollow' : 'Follow'}
    </button>
  );
}

Podstawy Server Components Pytania

Czym są Server Components i dlaczego zostały wprowadzone?

Server Components to komponenty które renderują się wyłącznie na serwerze. Ich kod nigdy nie trafia do bundle JavaScript klienta.

Problemy które rozwiązują:

  1. Bundle size - w tradycyjnej architekturze cały kod komponentów trafia do klienta, nawet biblioteki używane tylko do formatowania danych
  2. Waterfall requests - klient musi najpierw pobrać JS, wykonać go, potem pobrać dane z API
  3. Duplikacja logiki - często piszemy API endpoint tylko po to żeby komponent mógł pobrać dane
  4. Bezpieczeństwo - sekrety (API keys, connection strings) muszą być ukryte za API
// Tradycyjna architektura
// 1. Klient pobiera bundle z komponentem
// 2. Komponent renderuje się, wywołuje useEffect
// 3. useEffect wywołuje fetch('/api/users')
// 4. API endpoint pobiera dane z bazy
// 5. Dane wracają do klienta, komponent re-renderuje

// Server Components
// 1. Serwer renderuje komponent z danymi
// 2. Klient otrzymuje gotowy HTML + RSC Payload
// Koniec - bez dodatkowych requestów, bez kodu komponentu w bundle

Server Component vs Client Component - jakie różnice?

Cecha Server Component Client Component
Rendering Tylko serwer SSR + hydration na kliencie
Bundle Nie w bundle W bundle klienta
useState/useEffect
Event handlers
Browser APIs
async/await ✅ bezpośrednio ❌ (wymaga useEffect/hook)
Dostęp do DB/filesystem
Sekrety (env vars) ❌ (tylko NEXT_PUBLIC_)
// Server Component - async, bezpośredni dostęp do danych
async function ProductList() {
  const products = await db.products.findMany(); // Bezpośrednio z bazy

  return (
    <ul>
      {products.map(product => (
        <li key={product.id}>{product.name}</li>
      ))}
    </ul>
  );
}

// Client Component - interaktywność
"use client";

function AddToCartButton({ productId }: { productId: string }) {
  const [isAdding, setIsAdding] = useState(false);

  const handleClick = async () => {
    setIsAdding(true);
    await addToCart(productId);
    setIsAdding(false);
  };

  return (
    <button onClick={handleClick} disabled={isAdding}>
      {isAdding ? 'Adding...' : 'Add to Cart'}
    </button>
  );
}

Jak działa dyrektywa "use client"?

"use client" na początku pliku oznacza że komponent jest Client Component. Tworzy "granicę" (boundary) między serwerem a klientem.

// components/Counter.tsx
"use client"; // Musi być na samym początku pliku

import { useState } from 'react';

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

Ważne zasady:

  1. Granica propaguje w dół - wszystkie komponenty importowane przez Client Component są też Client Components
"use client";

// Te komponenty też będą Client Components
// nawet bez "use client" w ich plikach
import { ChildA } from './ChildA';
import { ChildB } from './ChildB';
  1. "use client" nie oznacza "tylko klient" - Client Components nadal są SSR dla pierwszego ładowania
  2. Nie można importować Server Component do Client Component - ale można przekazać jako children
// ❌ NIE DZIAŁA
"use client";
import { ServerComponent } from './ServerComponent';

function ClientComponent() {
  return <ServerComponent />; // Error!
}

// ✅ DZIAŁA - Server Component jako children
// page.tsx (Server Component)
import { ClientComponent } from './ClientComponent';
import { ServerComponent } from './ServerComponent';

function Page() {
  return (
    <ClientComponent>
      <ServerComponent />
    </ClientComponent>
  );
}

Co to jest RSC Payload?

RSC Payload to specjalny format danych który serwer wysyła do klienta. Zawiera:

  1. Wyrenderowany wynik Server Components
  2. Placeholdery gdzie powinny być Client Components
  3. Props dla Client Components
  4. Instrukcje jak złożyć całość
RSC Payload (uproszczony):
{
  "type": "ServerComponent",
  "props": {},
  "children": [
    { "type": "h1", "children": "Hello World" },
    {
      "type": "ClientComponent",
      "reference": "components/Counter.js",
      "props": { "initialCount": 0 }
    }
  ]
}

React na kliencie:

  1. Parsuje RSC Payload
  2. Renderuje statyczne części (Server Components)
  3. Ładuje i hydruje Client Components
  4. Łączy wszystko w jedno drzewo

Data Fetching w Server Components Pytania

Jak pobierać dane w Server Components?

Server Components mogą być async - używasz await bezpośrednio w komponencie:

// app/users/page.tsx
async function UsersPage() {
  // Bezpośrednio w komponencie - bez useEffect, bez useState
  const users = await fetch('https://api.example.com/users').then(r => r.json());

  return (
    <ul>
      {users.map((user: User) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

Bezpośredni dostęp do bazy:

import { prisma } from '@/lib/prisma';

async function ProductsPage() {
  const products = await prisma.product.findMany({
    where: { isActive: true },
    orderBy: { createdAt: 'desc' },
    take: 20
  });

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

Dostęp do systemu plików:

import { readFile } from 'fs/promises';
import path from 'path';

async function MarkdownPage({ slug }: { slug: string }) {
  const filePath = path.join(process.cwd(), 'content', `${slug}.md`);
  const content = await readFile(filePath, 'utf-8');

  return <Markdown content={content} />;
}

Jak działa caching w Next.js z Server Components?

Next.js rozszerza fetch o automatyczne caching:

// Domyślnie: cache forever (static)
const data = await fetch('https://api.example.com/data');

// Revalidate co 60 sekund
const data = await fetch('https://api.example.com/data', {
  next: { revalidate: 60 }
});

// Bez cache (dynamic)
const data = await fetch('https://api.example.com/data', {
  cache: 'no-store'
});

// Cache z tagiem do manual revalidation
const data = await fetch('https://api.example.com/products', {
  next: { tags: ['products'] }
});

// Revalidate tagu (np. po dodaniu produktu)
import { revalidateTag } from 'next/cache';
revalidateTag('products');

Dla nie-fetch data sources (Prisma, etc.):

import { unstable_cache } from 'next/cache';

const getProducts = unstable_cache(
  async () => {
    return await prisma.product.findMany();
  },
  ['products'], // Cache key
  {
    revalidate: 60,
    tags: ['products']
  }
);

async function ProductsPage() {
  const products = await getProducts();
  return <ProductList products={products} />;
}

Jak obsługiwać równoległe requesty?

// ❌ Sekwencyjne - wolne
async function Dashboard() {
  const user = await getUser();
  const posts = await getPosts(); // Czeka na getUser
  const comments = await getComments(); // Czeka na getPosts

  return <DashboardUI user={user} posts={posts} comments={comments} />;
}

// ✅ Równoległe - szybkie
async function Dashboard() {
  const [user, posts, comments] = await Promise.all([
    getUser(),
    getPosts(),
    getComments()
  ]);

  return <DashboardUI user={user} posts={posts} comments={comments} />;
}

// ✅ Jeszcze lepiej - streaming z Suspense
async function Dashboard() {
  const userPromise = getUser();

  return (
    <div>
      <Suspense fallback={<UserSkeleton />}>
        <UserSection userPromise={userPromise} />
      </Suspense>
      <Suspense fallback={<PostsSkeleton />}>
        <PostsSection />
      </Suspense>
      <Suspense fallback={<CommentsSkeleton />}>
        <CommentsSection />
      </Suspense>
    </div>
  );
}

async function UserSection({ userPromise }: { userPromise: Promise<User> }) {
  const user = await userPromise;
  return <UserCard user={user} />;
}

Streaming i Suspense Pytania

Jak działa streaming z Server Components?

Streaming pozwala serwerowi wysyłać części strony jak tylko są gotowe, zamiast czekać na wszystko:

// app/page.tsx
import { Suspense } from 'react';

export default function Page() {
  return (
    <div>
      {/* Natychmiast - statyczny content */}
      <Header />

      {/* Streamowany - pokazuje skeleton, potem dane */}
      <Suspense fallback={<ProductsSkeleton />}>
        <ProductsSection />
      </Suspense>

      {/* Streamowany niezależnie */}
      <Suspense fallback={<ReviewsSkeleton />}>
        <ReviewsSection />
      </Suspense>

      {/* Natychmiast */}
      <Footer />
    </div>
  );
}

async function ProductsSection() {
  // Ta część jest streamowana
  const products = await fetchProducts(); // Wolne API
  return <ProductGrid products={products} />;
}

Jak to działa pod spodem:

  1. Serwer wysyła początkowy HTML z Header, Footer, i placeholdery (skeletons)
  2. Przeglądarka renderuje to natychmiast
  3. Gdy ProductsSection się załaduje, serwer wysyła HTML + JS który zamienia skeleton
  4. To samo dla ReviewsSection (niezależnie)

Nested Suspense boundaries

export default function ProductPage({ params }: { params: { id: string } }) {
  return (
    <div>
      <Suspense fallback={<ProductSkeleton />}>
        <ProductDetails productId={params.id}>
          {/* Nested Suspense - ładuje się po ProductDetails */}
          <Suspense fallback={<ReviewsSkeleton />}>
            <ProductReviews productId={params.id} />
          </Suspense>
        </ProductDetails>
      </Suspense>
    </div>
  );
}

async function ProductDetails({
  productId,
  children
}: {
  productId: string;
  children: React.ReactNode
}) {
  const product = await fetchProduct(productId);

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

Loading.tsx i Error.tsx w Next.js

Next.js automatycznie tworzy Suspense boundaries z plików specjalnych:

app/
  products/
    page.tsx        # Strona
    loading.tsx     # Automatyczny Suspense fallback
    error.tsx       # Error boundary
    not-found.tsx   # 404 handling
// app/products/loading.tsx
export default function Loading() {
  return <ProductsPageSkeleton />;
}

// app/products/error.tsx
"use client"; // Error boundary musi być Client Component

export default function Error({
  error,
  reset
}: {
  error: Error & { digest?: string };
  reset: () => void;
}) {
  return (
    <div>
      <h2>Something went wrong!</h2>
      <button onClick={() => reset()}>Try again</button>
    </div>
  );
}

Server Actions Pytania

Czym są Server Actions i jak działają?

Server Actions to funkcje async oznaczone "use server" które wykonują się na serwerze ale mogą być wywoływane z klienta:

// app/actions.ts
"use server";

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

export async function createPost(formData: FormData) {
  const title = formData.get('title') as string;
  const content = formData.get('content') as string;

  await prisma.post.create({
    data: { title, content }
  });

  revalidatePath('/posts');
}

export async function deletePost(postId: string) {
  await prisma.post.delete({
    where: { id: postId }
  });

  revalidatePath('/posts');
}

Użycie w formularzach:

// app/posts/new/page.tsx
import { createPost } from '@/app/actions';

export default function NewPostPage() {
  return (
    <form action={createPost}>
      <input name="title" placeholder="Title" required />
      <textarea name="content" placeholder="Content" required />
      <button type="submit">Create Post</button>
    </form>
  );
}

Server Actions w Client Components

"use client";

import { createPost } from '@/app/actions';
import { useFormStatus } from 'react-dom';
import { useActionState } from 'react';

function SubmitButton() {
  const { pending } = useFormStatus();

  return (
    <button type="submit" disabled={pending}>
      {pending ? 'Creating...' : 'Create Post'}
    </button>
  );
}

export function CreatePostForm() {
  return (
    <form action={createPost}>
      <input name="title" required />
      <textarea name="content" required />
      <SubmitButton />
    </form>
  );
}

// Z useActionState dla obsługi błędów i stanu
export function CreatePostFormWithState() {
  const [state, formAction, isPending] = useActionState(
    async (prevState: any, formData: FormData) => {
      try {
        await createPost(formData);
        return { success: true };
      } catch (error) {
        return { error: 'Failed to create post' };
      }
    },
    null
  );

  return (
    <form action={formAction}>
      {state?.error && <p className="error">{state.error}</p>}
      {state?.success && <p className="success">Post created!</p>}
      <input name="title" required />
      <textarea name="content" required />
      <button disabled={isPending}>
        {isPending ? 'Creating...' : 'Create'}
      </button>
    </form>
  );
}

Server Actions jako event handlers

"use client";

import { deletePost } from '@/app/actions';
import { useTransition } from 'react';

export function DeleteButton({ postId }: { postId: string }) {
  const [isPending, startTransition] = useTransition();

  const handleDelete = () => {
    startTransition(async () => {
      await deletePost(postId);
    });
  };

  return (
    <button onClick={handleDelete} disabled={isPending}>
      {isPending ? 'Deleting...' : 'Delete'}
    </button>
  );
}

// Lub z bind dla przekazania argumentów
import { deletePost } from '@/app/actions';

export function DeleteButton({ postId }: { postId: string }) {
  const deletePostWithId = deletePost.bind(null, postId);

  return (
    <form action={deletePostWithId}>
      <button type="submit">Delete</button>
    </form>
  );
}

Walidacja i obsługa błędów w Server Actions

"use server";

import { z } from 'zod';
import { revalidatePath } from 'next/cache';

const PostSchema = z.object({
  title: z.string().min(1, 'Title is required').max(100),
  content: z.string().min(10, 'Content must be at least 10 characters')
});

export type ActionState = {
  errors?: {
    title?: string[];
    content?: string[];
  };
  message?: string;
  success?: boolean;
};

export async function createPost(
  prevState: ActionState,
  formData: FormData
): Promise<ActionState> {
  // Walidacja z Zod
  const validatedFields = PostSchema.safeParse({
    title: formData.get('title'),
    content: formData.get('content')
  });

  if (!validatedFields.success) {
    return {
      errors: validatedFields.error.flatten().fieldErrors,
      message: 'Invalid fields'
    };
  }

  try {
    await prisma.post.create({
      data: validatedFields.data
    });

    revalidatePath('/posts');
    return { success: true, message: 'Post created!' };
  } catch (error) {
    return { message: 'Database error' };
  }
}
"use client";

import { useActionState } from 'react';
import { createPost, ActionState } from '@/app/actions';

export function CreatePostForm() {
  const [state, formAction, isPending] = useActionState<ActionState, FormData>(
    createPost,
    { errors: {} }
  );

  return (
    <form action={formAction}>
      <div>
        <input name="title" />
        {state.errors?.title && (
          <p className="error">{state.errors.title[0]}</p>
        )}
      </div>

      <div>
        <textarea name="content" />
        {state.errors?.content && (
          <p className="error">{state.errors.content[0]}</p>
        )}
      </div>

      {state.message && (
        <p className={state.success ? 'success' : 'error'}>
          {state.message}
        </p>
      )}

      <button disabled={isPending}>
        {isPending ? 'Creating...' : 'Create Post'}
      </button>
    </form>
  );
}

Composition Patterns Pytania

Jak komponować Server i Client Components?

Pattern 1: Server Component z Client Component children

// app/page.tsx (Server Component)
import { ClientSidebar } from './ClientSidebar';

async function Page() {
  const user = await getUser();

  return (
    <div className="layout">
      <ClientSidebar user={user} />
      <main>
        <h1>Welcome, {user.name}</h1>
        {/* Więcej Server Components */}
      </main>
    </div>
  );
}

Pattern 2: Server Component jako children Client Component

// Modal.tsx (Client Component)
"use client";

import { useState } from 'react';

export function Modal({ children }: { children: React.ReactNode }) {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <>
      <button onClick={() => setIsOpen(true)}>Open</button>
      {isOpen && (
        <div className="modal">
          {children} {/* Server Component może być tutaj */}
          <button onClick={() => setIsOpen(false)}>Close</button>
        </div>
      )}
    </>
  );
}

// page.tsx (Server Component)
import { Modal } from './Modal';
import { ServerContent } from './ServerContent';

function Page() {
  return (
    <Modal>
      <ServerContent /> {/* Ten Server Component renderuje się na serwerze */}
    </Modal>
  );
}

Pattern 3: Slot pattern dla dynamicznego content

// Dashboard.tsx (Client Component)
"use client";

import { useState } from 'react';

export function Dashboard({
  sidebar,
  main,
  stats
}: {
  sidebar: React.ReactNode;
  main: React.ReactNode;
  stats: React.ReactNode;
}) {
  const [collapsed, setCollapsed] = useState(false);

  return (
    <div className="dashboard">
      {!collapsed && <aside>{sidebar}</aside>}
      <main>
        <button onClick={() => setCollapsed(!collapsed)}>
          Toggle Sidebar
        </button>
        {main}
      </main>
      <aside>{stats}</aside>
    </div>
  );
}

// page.tsx (Server Component)
import { Dashboard } from './Dashboard';

async function Page() {
  return (
    <Dashboard
      sidebar={<ServerSidebar />}
      main={<ServerMainContent />}
      stats={<ServerStats />}
    />
  );
}

Kiedy używać Server vs Client Components?

Używaj Server Components dla:

  • Fetching danych
  • Dostęp do backendu (DB, filesystem, API z sekretami)
  • Ciężkie zależności (markdown parsers, syntax highlighters)
  • Statyczny content
  • SEO-critical content

Używaj Client Components dla:

  • Interaktywność (onClick, onChange)
  • State (useState, useReducer)
  • Effects (useEffect, useLayoutEffect)
  • Browser APIs (localStorage, geolocation)
  • Custom hooks z state/effects
  • React Class components
// ✅ Server Component - ciężka biblioteka nie trafia do bundle
import { marked } from 'marked';
import hljs from 'highlight.js';

async function MarkdownRenderer({ content }: { content: string }) {
  const html = marked(content, {
    highlight: (code, lang) => hljs.highlight(code, { language: lang }).value
  });

  return <div dangerouslySetInnerHTML={{ __html: html }} />;
}

// ✅ Client Component - interaktywność
"use client";

function LikeButton({ postId }: { postId: string }) {
  const [likes, setLikes] = useState(0);

  return (
    <button onClick={() => setLikes(l => l + 1)}>
      ❤️ {likes}
    </button>
  );
}

// ✅ Kombinacja
async function BlogPost({ slug }: { slug: string }) {
  const post = await getPost(slug);

  return (
    <article>
      <h1>{post.title}</h1>
      <MarkdownRenderer content={post.content} />
      <LikeButton postId={post.id} />
    </article>
  );
}

Props serialization między Server i Client Components

Props przekazywane z Server do Client Components muszą być serializowalne:

// ✅ Dozwolone
<ClientComponent
  string="hello"
  number={42}
  boolean={true}
  array={[1, 2, 3]}
  object={{ name: "John" }}
  date={date.toISOString()} // Jako string, nie Date object
/>

// ❌ Niedozwolone
<ClientComponent
  function={() => console.log('hi')} // Funkcje nie są serializowalne
  class={new MyClass()} // Instancje klas
  symbol={Symbol('test')} // Symbole
  date={new Date()} // Date objects
/>

// Rozwiązanie dla funkcji - Server Actions
"use server";
export async function handleAction(data: FormData) {
  // ...
}

// Client Component może używać Server Action
"use client";
import { handleAction } from './actions';

function Form() {
  return <form action={handleAction}>...</form>;
}

Performance i Optymalizacja Pytania

Jakie są korzyści performance z RSC?

  1. Mniejszy bundle JavaScript:
// Przed RSC: marked (37KB) + highlight.js (40KB) w bundle klienta
// Po RSC: 0KB w bundle - biblioteki są tylko na serwerze

async function CodeBlock({ code, language }: Props) {
  // Te importy nie trafiają do klienta
  const { marked } = await import('marked');
  const hljs = await import('highlight.js');

  const html = marked(code);
  return <pre dangerouslySetInnerHTML={{ __html: html }} />;
}
  1. Szybszy initial load (TTFB, FCP):
  • Mniej JS do pobrania i parsowania
  • Streaming pozwala na progresywny rendering
  • Dane pobierane na serwerze (blisko bazy danych)
  1. Automatyczny code splitting:
  • Każdy Client Component to oddzielny chunk
  • Ładowany tylko gdy potrzebny
  1. Lepsze caching:
  • Server Components mogą być cache'owane na edge
  • Static rendering dla stron bez dynamic data

Jak profilować i optymalizować RSC?

// 1. Identyfikuj wolne fetche
async function SlowComponent() {
  console.time('fetch');
  const data = await fetchData(); // Zmierz czas
  console.timeEnd('fetch');
  return <div>{data}</div>;
}

// 2. Używaj parallel fetching
async function Dashboard() {
  // ❌ Sekwencyjne
  const a = await fetchA();
  const b = await fetchB();

  // ✅ Równoległe
  const [a, b] = await Promise.all([fetchA(), fetchB()]);

  return <div>{a}{b}</div>;
}

// 3. Suspense dla perceived performance
function Page() {
  return (
    <div>
      {/* Pokazuj cokolwiek natychmiast */}
      <Header />

      {/* Streamuj wolne części */}
      <Suspense fallback={<Skeleton />}>
        <SlowDataSection />
      </Suspense>
    </div>
  );
}

// 4. Preload data
import { preload } from 'react-dom';

async function Page() {
  // Zacznij ładować dane jak najwcześniej
  preload('/api/data', { as: 'fetch' });

  return <Content />;
}

Static vs Dynamic Rendering

// Static Rendering (domyślne) - generowane w build time
// Używane gdy nie ma dynamic danych
async function StaticPage() {
  const posts = await getStaticPosts(); // Cache'd forever
  return <PostList posts={posts} />;
}

// Dynamic Rendering - generowane per-request
// Wymuszone przez:
// - cookies(), headers()
// - searchParams
// - fetch z { cache: 'no-store' }

import { cookies } from 'next/headers';

async function DynamicPage() {
  const session = cookies().get('session'); // Wymusza dynamic
  const user = await getUser(session?.value);
  return <UserDashboard user={user} />;
}

// Partial Prerendering (PPR) - hybrid
// Statyczne części + dynamic holes
export const experimental_ppr = true;

async function HybridPage() {
  return (
    <div>
      <StaticHeader /> {/* Pre-rendered */}
      <Suspense fallback={<Skeleton />}>
        <DynamicUserSection /> {/* Streamowane */}
      </Suspense>
      <StaticFooter /> {/* Pre-rendered */}
    </div>
  );
}

Migracja i Adoption Pytania

Jak migrować z Pages Router do App Router?

// PRZED: pages/posts/[id].tsx
import { GetServerSideProps } from 'next';

export const getServerSideProps: GetServerSideProps = async ({ params }) => {
  const post = await fetchPost(params?.id as string);
  return { props: { post } };
};

export default function PostPage({ post }: { post: Post }) {
  return <PostContent post={post} />;
}

// PO: app/posts/[id]/page.tsx
async function PostPage({ params }: { params: { id: string } }) {
  const post = await fetchPost(params.id);
  return <PostContent post={post} />;
}

export default PostPage;

Stopniowa migracja:

  1. Utwórz folder app/ obok pages/
  2. Przenoś route po route
  3. Współistnieją - App Router ma priorytet
  4. Client Components oznacz "use client"

Jak migrować useState/useEffect do Server Components?

// PRZED: Client Component z data fetching
"use client";

function UserProfile({ userId }: { userId: string }) {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    fetchUser(userId).then(setUser).finally(() => setLoading(false));
  }, [userId]);

  if (loading) return <Skeleton />;
  if (!user) return <NotFound />;

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

// PO: Server Component
async function UserProfile({ userId }: { userId: string }) {
  const user = await fetchUser(userId);

  if (!user) {
    notFound(); // Next.js helper
  }

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

// Wrapper dla Suspense
function UserProfilePage({ userId }: { userId: string }) {
  return (
    <Suspense fallback={<Skeleton />}>
      <UserProfile userId={userId} />
    </Suspense>
  );
}

Common mistakes przy używaniu RSC

// ❌ Używanie hooks w Server Component
async function ServerComponent() {
  const [state, setState] = useState(0); // Error!
  useEffect(() => {}, []); // Error!
  return <div>{state}</div>;
}

// ❌ Bezpośredni import Server Component w Client Component
"use client";
import { ServerThing } from './ServerThing'; // Nie zadziała

function ClientComponent() {
  return <ServerThing />; // Error!
}

// ✅ Przekaż jako children
function Parent() {
  return (
    <ClientComponent>
      <ServerThing />
    </ClientComponent>
  );
}

// ❌ Przekazywanie nieserializowalnych props
async function Page() {
  const handleClick = () => console.log('clicked');
  return <ClientButton onClick={handleClick} />; // Error!
}

// ✅ Użyj Server Action
async function Page() {
  async function handleClick() {
    "use server";
    console.log('clicked on server');
  }
  return <ClientButton onClick={handleClick} />;
}

// ❌ Używanie "use server" w komponencie
function Component() {
  "use server"; // To nie działa - tylko w funkcjach async
  return <div />;
}

// ✅ "use server" w osobnym pliku lub w async function
// actions.ts
"use server";
export async function myAction() { ... }

Zaawansowane Patterns Pytania

Optimistic Updates z Server Actions

"use client";

import { useOptimistic } from 'react';
import { addTodo } from '@/app/actions';

type Todo = { id: string; text: string; completed: boolean };

export function TodoList({ todos }: { todos: Todo[] }) {
  const [optimisticTodos, addOptimisticTodo] = useOptimistic(
    todos,
    (state, newTodo: Todo) => [...state, newTodo]
  );

  async function handleSubmit(formData: FormData) {
    const text = formData.get('text') as string;

    // Natychmiast pokaż nowy todo (optimistic)
    addOptimisticTodo({
      id: `temp-${Date.now()}`,
      text,
      completed: false
    });

    // Wyślij na serwer (w tle)
    await addTodo(formData);
  }

  return (
    <div>
      <ul>
        {optimisticTodos.map(todo => (
          <li key={todo.id}>{todo.text}</li>
        ))}
      </ul>
      <form action={handleSubmit}>
        <input name="text" required />
        <button type="submit">Add</button>
      </form>
    </div>
  );
}

Infinite Scroll z Server Components

// app/posts/page.tsx
import { Suspense } from 'react';
import { LoadMoreButton } from './LoadMoreButton';

async function PostsPage({
  searchParams
}: {
  searchParams: { page?: string }
}) {
  const page = Number(searchParams.page) || 1;
  const posts = await fetchPosts({ page, limit: 10 });

  return (
    <div>
      {posts.map(post => (
        <PostCard key={post.id} post={post} />
      ))}

      {posts.length === 10 && (
        <LoadMoreButton currentPage={page} />
      )}
    </div>
  );
}

// LoadMoreButton.tsx
"use client";

import { useRouter, useSearchParams } from 'next/navigation';
import { useTransition } from 'react';

export function LoadMoreButton({ currentPage }: { currentPage: number }) {
  const router = useRouter();
  const [isPending, startTransition] = useTransition();

  const loadMore = () => {
    startTransition(() => {
      router.push(`?page=${currentPage + 1}`, { scroll: false });
    });
  };

  return (
    <button onClick={loadMore} disabled={isPending}>
      {isPending ? 'Loading...' : 'Load More'}
    </button>
  );
}

Real-time Updates z Server Components

// Polling approach
"use client";

import { useRouter } from 'next/navigation';
import { useEffect } from 'react';

export function Poller({ interval = 5000 }: { interval?: number }) {
  const router = useRouter();

  useEffect(() => {
    const id = setInterval(() => {
      router.refresh(); // Odświeża Server Components
    }, interval);

    return () => clearInterval(id);
  }, [interval, router]);

  return null;
}

// Użycie
async function NotificationsPage() {
  const notifications = await getNotifications();

  return (
    <div>
      <Poller interval={10000} />
      <NotificationList notifications={notifications} />
    </div>
  );
}

// Alternatywa: Server-Sent Events w Client Component
"use client";

export function RealtimeNotifications() {
  const [notifications, setNotifications] = useState<Notification[]>([]);

  useEffect(() => {
    const eventSource = new EventSource('/api/notifications/stream');

    eventSource.onmessage = (event) => {
      const notification = JSON.parse(event.data);
      setNotifications(prev => [notification, ...prev]);
    };

    return () => eventSource.close();
  }, []);

  return <NotificationList notifications={notifications} />;
}

Zadania Praktyczne

1. Zbuduj blog z Server Components

Wymagania:

  • Lista postów (Server Component)
  • Szczegóły posta z Markdown rendering (Server Component)
  • Komentarze z paginacją (streaming)
  • Formularz dodawania komentarza (Server Action)
  • Like button (Client Component z optimistic update)

2. Dashboard z real-time data

Wymagania:

  • Statystyki ładowane równolegle (Promise.all)
  • Wykresy jako Client Components
  • Auto-refresh co 30 sekund
  • Loading states dla każdej sekcji

3. E-commerce product page

Wymagania:

  • Product details (Server Component, cached)
  • Related products (streaming)
  • Add to cart (Server Action)
  • Image gallery (Client Component)
  • Reviews z infinite scroll

Powiązane Artykuły

Powrót do blogu

Zostaw komentarz

Pamiętaj, że komentarze muszą zostać zatwierdzone przed ich opublikowaniem.