React Server Components - Pytania Rekrutacyjne i Kompletny Przewodnik 2025

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.

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

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

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

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

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

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

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

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

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

Podsumowanie

React Server Components to fundamentalna zmiana w architekturze React aplikacji. Na rozmowach rekrutacyjnych skup się na:

  1. Podstawy - różnica Server vs Client Components, "use client"
  2. Data fetching - async components, caching, revalidation
  3. Streaming - Suspense boundaries, loading.tsx, parallel fetching
  4. Server Actions - "use server", form handling, mutations
  5. Composition - patterns łączenia Server i Client Components
  6. Performance - bundle size, TTFB, static vs dynamic rendering
  7. Migracja - z Pages Router, z Client-side fetching

RSC zmieniają sposób myślenia o React - nie wszystko musi być interaktywne na kliencie. Domyślnie renderuj na serwerze, dodawaj "use client" tylko gdy potrzebujesz interaktywności.

Powrót do blogu

Zostaw komentarz

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