Kurs JavaScript i React · Moduł 10: Ekosystem i przyszłość React
React Server Components (RSC) - przyszłość React
W tej lekcji6
Panel misji pokazuje posty z bazy danych i przycisk polubienia. W klasycznej aplikacji cały kod komponentów, razem z bibliotekami i logiką zapytań, trafia do przeglądarki, a dane dociągasz dopiero po załadowaniu strony. React Server Components odwracają ten podział: część komponentów wykonuje się wyłącznie na serwerze, ma bezpośredni dostęp do danych i nie dodaje ani bajta do paczki JavaScript. To fundamentalna zmiana w myśleniu o aplikacjach React.
Czym są React Server Components?
Server Component renderuje się na serwerze, podczas budowania albo przy każdym żądaniu. Jego wynikiem nie jest gotowy HTML, tylko RSC Payload, czyli zserializowany opis drzewa UI z miejscami na komponenty klienckie. Cykl żądania wygląda tak: klient prosi o stronę, komponenty serwerowe pobierają dane i tworzą payload, klient buduje z niego drzewo, a na końcu komponenty klienckie są hydratowane, czyli dostają zdarzenia i stan. HTML może dodatkowo powstać przez SSR.
Różnice między RSC a SSR
Klasyczny SSR renderuje całą aplikację do HTML, a potem i tak wysyła cały jej kod do przeglądarki na hydratację. RSC pozwala mieszać komponenty serwerowe i klienckie w jednym drzewie:
1// Klasyczny SSR (Pages Router) - dane przychodzą z getServerSideProps
2export async function getServerSideProps() {
3 const posts = await fetchPosts(); // Wykonuje się na serwerze przy każdym żądaniu
4 return { props: { posts } };
5}
6
7export default function TraditionalSSRPage({ posts }) {
8 const [count, setCount] = useState(0);
9
10 // Ten komponent razem ze wszystkimi importami trafia też do przeglądarki
11 return (
12 <div>
13 <PostsList posts={posts} />
14 <Counter count={count} setCount={setCount} />
15 </div>
16 );
17}
18
19// React Server Components - mix serwera i klienta
20async function ServerComponentPage() {
21 // Ten kod działa TYLKO na serwerze
22 const posts = await fetchPosts(); // Bezpośrednie wywołanie bazy danych
23 const user = await getUser();
24
25 return (
26 <div>
27 {/* Server Component - renderowany na serwerze */}
28 <PostsList posts={posts} />
29
30 {/* Client Component - interaktywny na kliencie */}
31 <Counter />
32
33 {/* Server Component z danymi z serwera */}
34 <UserProfile user={user} />
35 </div>
36 );
37}W klasycznym SSR dane pobiera osobna funkcja getServerSideProps, a komponent strony dostaje je w propsach. Serwer renderuje z nich HTML, ale kod TraditionalSSRPage, PostsList i wszystkich ich bibliotek i tak trafia do przeglądarki, żeby React mógł zhydratować stronę. ServerComponentPage jest asynchroniczna i czeka na dane sama: jej kod zostaje na serwerze, a do przeglądarki trafia tylko interaktywny Counter.
Podstawy Server Components
W Next.js App Router każdy komponent jest domyślnie serwerowy. Dyrektywa 'use client' na samej górze pliku wyznacza granicę: ten moduł i jego importy trafiają do przeglądarki i mogą używać hooków, zdarzeń i stanu:
1// ServerComponent.jsx (domyślnie server component)
2import { db } from './database';
3
4export default async function PostsList() {
5 // Bezpośredni dostęp do bazy danych
6 const posts = await db.posts.findMany({
7 include: { author: true },
8 orderBy: { createdAt: 'desc' }
9 });
10
11 return (
12 <div className="posts-list">
13 {posts.map(post => (
14 <article key={post.id}>
15 <h2>{post.title}</h2>
16 <p>By {post.author.name}</p>
17 <p>{post.excerpt}</p>
18
19 {/* Client Component dla interakcji */}
20 <LikeButton postId={post.id} initialLikes={post.likes} />
21 </article>
22 ))}
23 </div>
24 );
25}
26
27// LikeButton.jsx (client component)
28'use client'; // Dyrektywa oznaczająca client component
29
30import { useState } from 'react';
31
32export default function LikeButton({ postId, initialLikes }) {
33 const [likes, setLikes] = useState(initialLikes);
34 const [isLiking, setIsLiking] = useState(false);
35
36 const handleLike = async () => {
37 setIsLiking(true);
38 try {
39 const response = await fetch(`/api/posts/${postId}/like`, {
40 method: 'POST'
41 });
42 const data = await response.json();
43 setLikes(data.likes);
44 } catch (error) {
45 console.error('Failed to like post:', error);
46 } finally {
47 setIsLiking(false);
48 }
49 };
50
51 return (
52 <button
53 onClick={handleLike}
54 disabled={isLiking}
55 className="like-button"
56 >
57 {likes} {isLiking && '...'}
58 </button>
59 );
60}PostsList pyta bazę bezpośrednio, a LikeButton obsługuje kliknięcia przez useState. Komponent serwerowy renderuje kliencki jako dziecko i przekazuje mu serializowalne propsy, jak postId. W projekcie to dwa osobne pliki, bo dyrektywa działa tylko na początku pliku.
Pobieranie danych w Server Components
Server Component może bezpiecznie używać sekretów i zapytań do bazy, bo jego kod nigdy nie trafia do przeglądarki. Funkcja cache z Reacta zapamiętuje wynik w obrębie jednego żądania:
1// utils/api.js (server-side only)
2import { cache } from 'react';
3
4// Wynik zapamiętany w obrębie jednego żądania
5export const getPosts = cache(async () => {
6 const response = await fetch('https://api.example.com/posts', {
7 // Server Components mogą bezpiecznie używać sekretów
8 headers: {
9 'Authorization': `Bearer ${process.env.API_SECRET}`
10 }
11 });
12 return response.json();
13});
14
15export const getUser = cache(async (id) => {
16 // Bezpośrednie zapytanie do bazy danych
17 return await db.user.findUnique({
18 where: { id },
19 include: { profile: true }
20 });
21});
22
23// components/PostsPage.jsx
24import { getPosts, getUser } from '../utils/api';
25import { Suspense } from 'react';
26
27export default async function PostsPage({ userId }) {
28 // Równoległe pobieranie danych
29 const [posts, user] = await Promise.all([
30 getPosts(),
31 getUser(userId)
32 ]);
33
34 return (
35 <main>
36 <h1>Welcome, {user.name}!</h1>
37
38 {/* Zagnieżdżony Server Component z własnym pobieraniem danych */}
39 <Suspense fallback={<div>Loading posts...</div>}>
40 <PostsList posts={posts} />
41 </Suspense>
42
43 {/* Client Component dla interakcji */}
44 <CreatePostForm userId={userId} />
45 </main>
46 );
47}Promise.all pobiera posty i użytkownika równolegle. cache działa tylko w Server Components i deduplikuje wywołania z tymi samymi argumentami, więc kilka komponentów może wołać getUser(userId) bez dodatkowych zapytań.
Streaming i Suspense
Nie każdy fragment strony ładuje się równie szybko. Suspense z właściwością fallback wyznacza granicę: serwer wysyła najpierw zastępczy szkielet, a gotowe sekcje dosyła strumieniowo:
1// components/DashboardPage.jsx
2import { Suspense } from 'react';
3
4export default function DashboardPage() {
5 return (
6 <div className="dashboard">
7 <h1>Dashboard</h1>
8
9 {/* Szybko ładująca się treść */}
10 <Suspense fallback={<SkeletonCard />}>
11 <UserStats />
12 </Suspense>
13
14 {/* Wolniej ładująca się treść */}
15 <Suspense fallback={<SkeletonChart />}>
16 <AnalyticsChart />
17 </Suspense>
18
19 {/* Bardzo wolno ładująca się treść */}
20 <Suspense fallback={<SkeletonTable />}>
21 <ReportsTable />
22 </Suspense>
23 </div>
24 );
25}
26
27// components/UserStats.jsx (Server Component)
28async function UserStats() {
29 // Szybkie zapytanie - cache hit
30 const stats = await getUserStats();
31
32 return (
33 <div className="stats-grid">
34 <StatCard title="Total Users" value={stats.totalUsers} />
35 <StatCard title="Active Today" value={stats.activeToday} />
36 <StatCard title="Revenue" value={`$${stats.revenue}`} />
37 </div>
38 );
39}
40
41// components/AnalyticsChart.jsx (Server Component)
42async function AnalyticsChart() {
43 // Średnio długie zapytanie
44 const chartData = await getAnalyticsData();
45
46 return (
47 <div className="chart-container">
48 {/* Client Component dla interaktywnego wykresu */}
49 <InteractiveChart data={chartData} />
50 </div>
51 );
52}
53
54// components/ReportsTable.jsx (Server Component)
55async function ReportsTable() {
56 // Długie zapytanie - złożone agregacje
57 const reports = await getComplexReports();
58
59 return (
60 <div className="reports-table">
61 <table>
62 <thead>
63 <tr>
64 <th>Report</th>
65 <th>Status</th>
66 <th>Date</th>
67 </tr>
68 </thead>
69 <tbody>
70 {reports.map(report => (
71 <tr key={report.id}>
72 <td>{report.name}</td>
73 <td>{report.status}</td>
74 <td>{report.date}</td>
75 </tr>
76 ))}
77 </tbody>
78 </table>
79 </div>
80 );
81}Każda sekcja ma własną granicę, więc wolna tabela raportów nie blokuje statystyk. Suspense obsługuje stan ładowania, a błędy łapie Error Boundary, więc razem pokrywają cały cykl operacji asynchronicznej. To ten sam streaming, który poznałeś w lekcji o Streaming SSR.
Kompozycja Server i Client Components
Oto wzorce kompozycji, które działają, i jeden, który zawodzi:
1// patterns/CompositionPatterns.jsx
2
3// DOBRE - Server Component może renderować Client Component
4async function ServerParent() {
5 const data = await fetchData();
6
7 return (
8 <div>
9 <h1>Server Rendered Title</h1>
10 <ClientChild data={data} />
11 </div>
12 );
13}
14
15// DOBRE - Client Component może renderować Server Component przez children
16'use client';
17function ClientWrapper({ children }) {
18 const [isOpen, setIsOpen] = useState(false);
19
20 return (
21 <div>
22 <button onClick={() => setIsOpen(!isOpen)}>
23 Toggle Content
24 </button>
25 {isOpen && children}
26 </div>
27 );
28}
29
30// Użycie:
31function ParentPage() {
32 return (
33 <ClientWrapper>
34 <ServerContent /> {/* Server Component jako children */}
35 </ClientWrapper>
36 );
37}
38
39// ZŁE - Client Component nie może bezpośrednio importować Server Component
40'use client';
41function BadClientComponent() {
42 const [show, setShow] = useState(false);
43
44 return (
45 <div>
46 {show && <ServerContent />} {/* To nie zadziała! */}
47 </div>
48 );
49}
50
51// DOBRE - Server Component przekazany jako prop z JSX (slot)
52'use client';
53function GoodClientComponent({ content }) {
54 const [show, setShow] = useState(false);
55
56 return (
57 <div>
58 <button onClick={() => setShow(!show)}>Toggle</button>
59 {show && content}
60 </div>
61 );
62}
63
64// Użycie:
65function ParentComponent() {
66 return (
67 <GoodClientComponent
68 content={<ServerContent />}
69 />
70 );
71}Zalecany wzorzec to Server Component renderujący Client Component jako dziecko. Gdy kliencki ma zawierać serwerowy, przekaż go jako children albo prop z JSX. Funkcji, na przykład render propa, z serwera do klienta nie wyślesz, bo nie da się jej zserializować, a import serwerowego komponentu w pliku z 'use client' zamieni go w kliencki.
Zaawansowane wzorce
Mutacje przez Server Functions
Do zmiany danych służą Server Functions z dyrektywą 'use server', w formularzach nazywane Server Actions. Formularz wywołuje je bez pisania własnego endpointu:
1// actions/postActions.js
2'use server'; // Server Actions
3
4import { revalidatePath } from 'next/cache';
5
6export async function createPost(prevState, formData) {
7 const title = formData.get('title');
8 const content = formData.get('content');
9
10 // Walidacja po stronie serwera
11 if (!title || !content) {
12 return { error: 'Title and content are required' };
13 }
14
15 try {
16 const post = await db.post.create({
17 data: { title, content, userId: getCurrentUserId() }
18 });
19
20 // Odśwież cache strony z postami
21 revalidatePath('/posts');
22
23 return { success: true, post };
24 } catch (error) {
25 return { error: 'Failed to create post' };
26 }
27}
28
29export async function deletePost(postId) {
30 try {
31 await db.post.delete({
32 where: { id: postId }
33 });
34
35 revalidatePath('/posts');
36 return { success: true };
37 } catch (error) {
38 return { error: 'Failed to delete post' };
39 }
40}
41
42// components/CreatePostForm.jsx
43'use client';
44
45import { createPost } from '../actions/postActions';
46import { useActionState } from 'react';
47import { useFormStatus } from 'react-dom';
48
49function SubmitButton() {
50 const { pending } = useFormStatus();
51
52 return (
53 <button type="submit" disabled={pending}>
54 {pending ? 'Creating...' : 'Create Post'}
55 </button>
56 );
57}
58
59export default function CreatePostForm() {
60 const [state, formAction] = useActionState(createPost, null);
61
62 return (
63 <form action={formAction}>
64 <div>
65 <label htmlFor="title">Title:</label>
66 <input
67 type="text"
68 id="title"
69 name="title"
70 required
71 />
72 </div>
73
74 <div>
75 <label htmlFor="content">Content:</label>
76 <textarea
77 id="content"
78 name="content"
79 required
80 />
81 </div>
82
83 {state?.error && (
84 <div className="error">{state.error}</div>
85 )}
86
87 {state?.success && (
88 <div className="success">Post created successfully!</div>
89 )}
90
91 <SubmitButton />
92 </form>
93 );
94}useActionState przekazuje akcji poprzedni stan jako pierwszy argument, a dane formularza jako drugi, dlatego createPost przyjmuje prevState, formData. useFormStatus blokuje przycisk na czas wysyłki, a revalidatePath('/posts') odświeża listę. Walidacja w akcji jest obowiązkowa, bo akcja to publiczny endpoint.
Strategie cache
cache z Reacta działa tylko w obrębie jednego żądania. Żeby zapamiętać wynik kosztownego zapytania między żądaniami, Next.js 16 ma dyrektywę 'use cache', którą włączasz opcją cacheComponents: true w next.config.js. Czas życia wpisu ustawia cacheLife, a cacheTag nadaje mu etykietę do unieważniania:
1// lib/posts.js
2import { cacheLife, cacheTag } from 'next/cache';
3
4export async function getCachedPosts() {
5 'use cache';
6 cacheLife('minutes'); // Odświeżanie w tle mniej więcej co minutę
7 cacheTag('posts');
8
9 return db.post.findMany({
10 include: { author: true },
11 orderBy: { createdAt: 'desc' },
12 });
13}
14
15export async function getCachedUser(userId) {
16 'use cache';
17 cacheLife('hours');
18 cacheTag('users');
19
20 // userId staje się częścią klucza cache
21 return db.user.findUnique({
22 where: { id: userId },
23 include: { profile: true },
24 });
25}
26
27// components/PostsWithCache.jsx
28import { getCachedPosts } from '../lib/posts';
29
30export default async function PostsWithCache() {
31 const posts = await getCachedPosts();
32
33 return (
34 <div>
35 <h1>Posty z cache</h1>
36 {posts.map(post => (
37 <article key={post.id}>
38 <h2>{post.title}</h2>
39 <p>Autor: {post.author.name}</p>
40 </article>
41 ))}
42 </div>
43 );
44}
45
46// actions/cacheActions.js
47'use server';
48
49import { revalidateTag, updateTag } from 'next/cache';
50
51export async function refreshPosts() {
52 revalidateTag('posts', 'max'); // Stare dane zostają, świeże dociągną się w tle
53}
54
55export async function publishPost(formData) {
56 await db.post.create({ data: { title: formData.get('title') } });
57 updateTag('posts'); // Autor od razu zobaczy swój wpis
58}Argumenty funkcji, jak userId, automatycznie trafiają do klucza cache, więc każdy użytkownik ma osobny wpis. revalidateTag('posts', 'max') oznacza dane jako nieaktualne i odświeża je w tle przy następnym żądaniu, a updateTag, dostępne tylko w Server Actions, wygasza je od razu, żeby autor zobaczył swoją zmianę. Starsze unstable_cache zostało w Next.js 16 zastąpione przez 'use cache', a jednoargumentowe revalidateTag jest przestarzałe.
Performance i optymalizacje
Największego zysku z RSC nie widać w kodzie: komponenty serwerowe i ich importy nie trafiają do paczki klienta:
1// Server Components nie trafiają do paczki klienta!
2
3// ServerHeavyComponent.jsx (Server Component)
4import { someHeavyLibrary } from 'heavy-library'; // Nie trafi do paczki klienta!
5import { complexCalculation } from './utils/complex';
6
7export default async function ServerHeavyComponent() {
8 const data = await fetchData();
9 const processed = complexCalculation(data); // Dzieje się na serwerze
10 const result = someHeavyLibrary.process(processed);
11
12 return (
13 <div>
14 <h2>Processed Data</h2>
15 <pre>{JSON.stringify(result, null, 2)}</pre>
16 </div>
17 );
18}
19
20// ClientLightComponent.jsx (Client Component)
21'use client';
22
23import { useState } from 'react';
24
25export default function ClientLightComponent({ serverData }) {
26 const [selected, setSelected] = useState(null);
27
28 return (
29 <div>
30 {serverData.map(item => (
31 <button
32 key={item.id}
33 onClick={() => setSelected(item)}
34 className={selected?.id === item.id ? 'selected' : ''}
35 >
36 {item.name}
37 </button>
38 ))}
39 {selected && <ItemDetails item={selected} />}
40 </div>
41 );
42}heavy-library działa tylko na serwerze, więc użytkownik jej nie pobiera. Do przeglądarki trafia wyłącznie lekki ClientLightComponent.
Error Boundaries dla Server Components
Błąd w komponencie serwerowym trafia do najbliższej granicy błędu po stronie klienta. Error Boundary musi być klasą, więc plik zaczyna się od 'use client':
1// components/ServerErrorBoundary.jsx
2'use client';
3
4import { Component } from 'react';
5
6export class ServerErrorBoundary extends Component {
7 constructor(props) {
8 super(props);
9 this.state = { hasError: false, error: null };
10 }
11
12 static getDerivedStateFromError(error) {
13 return { hasError: true, error };
14 }
15
16 componentDidCatch(error, errorInfo) {
17 console.error('Server component error:', error, errorInfo);
18 // Wyślij do serwisu monitorującego
19 }
20
21 render() {
22 if (this.state.hasError) {
23 return (
24 <div className="error-boundary">
25 <h2>Something went wrong on the server</h2>
26 <details>
27 <summary>Error details</summary>
28 <pre>{this.state.error?.message}</pre>
29 </details>
30 <button
31 onClick={() => this.setState({ hasError: false })}
32 >
33 Try again
34 </button>
35 </div>
36 );
37 }
38
39 return this.props.children;
40 }
41}
42
43// Użycie w layoucie
44export default function Layout({ children }) {
45 return (
46 <html>
47 <body>
48 <ServerErrorBoundary>
49 {children}
50 </ServerErrorBoundary>
51 </body>
52 </html>
53 );
54}getDerivedStateFromError przełącza widok na ekran awaryjny, a componentDidCatch raportuje błąd. W produkcji Next.js ukrywa treść błędów z serwera, więc zobaczysz ogólny komunikat, a szczegóły znajdziesz w logach. W Next.js nie musisz pisać tej klasy sam: plik error.js tworzy granicę błędu dla całego segmentu trasy, a od wersji 16.3 funkcja catchError z next/error zbuduje ją z komponentu funkcyjnego i przekaże mu retry(), które ponownie renderuje także komponenty serwerowe.
Pierwszym frameworkiem, który w pełni zintegrował RSC, był Next.js z App Routerem. Polecam zaczynać każdy komponent jako serwerowy i dodawać 'use client' dopiero tam, gdzie potrzebujesz stanu lub zdarzeń. RSC dają lepszą wydajność, automatyczny podział kodu i bezpieczniejsze operacje na serwerze, a w lekcji o Partial Pre-rendering połączysz je ze statyczną powłoką strony.
Pamiętaj: Server Components to załoga stacji naziemnej - przygotowuje dane i wysyła gotowy plan, a na pokład trafiają tylko moduły, którymi ktoś naprawdę steruje. W przykładzie poniżej przycisk "Pokaż, co trafia do przeglądarki" oznacza, który kod zostaje na serwerze, a który działa w przeglądarce.
Kod do tej lekcji: App.jsx
1import { Suspense, use, useState } from 'react';
2
3// === React Server Components - symulacja w przeglądarce ===
4// Prawdziwy Server Component to asynchroniczna funkcja wykonywana na serwerze:
5// async function PostsList() { const posts = await db.post.findMany(); ... }
6// Przeglądarka nie uruchomi takiego kodu, więc "serwer" udajemy obietnicą
7// odczytywaną przez use(), a czekanie na dane obsługuje Suspense, tak jak przy
8// strumieniowaniu RSC Payload.
9
10const database = {
11 stats: { posts: 156, crew: 89, online: 23 },
12 posts: [
13 { id: 1, title: 'Odkrycie nowej galaktyki', author: 'Komandor Nova', likes: 42 },
14 { id: 2, title: 'Technologie napędów kosmicznych', author: 'Astro', likes: 28 },
15 { id: 3, title: 'Życie na Marsie - raport', author: 'Ra', likes: 56 },
16 ],
17};
18
19const serverCache = new Map();
20
21// "Zapytanie do bazy" - ta sama obietnica dla tej samej tabeli
22function queryServer(table, delay) {
23 if (!serverCache.has(table)) {
24 serverCache.set(table, new Promise(resolve => setTimeout(() => resolve(database[table]), delay)));
25 }
26 return serverCache.get(table);
27}
28
29function Tag({ type, show }) {
30 if (!show) return null;
31 const server = type === 'server';
32 return (
33 <span style={{ ...styles.tag, ...(server ? styles.serverTag : styles.clientTag) }}>
34 {server ? 'Server Component: 0 KB kodu w paczce JS' : "Client Component ('use client'): kod w paczce JS"}
35 </span>
36 );
37}
38
39// SERVER COMPONENT (symulacja): bez stanu i zdarzeń, tylko dane i JSX
40function ServerStats({ showBundle }) {
41 const stats = use(queryServer('stats', 600));
42
43 return (
44 <section style={{ ...styles.box, ...(showBundle ? styles.serverBox : null) }}>
45 <Tag type="server" show={showBundle} />
46 <div style={styles.statsGrid}>
47 <div style={styles.stat}><strong style={styles.statValue}>{stats.posts}</strong>Posty</div>
48 <div style={styles.stat}><strong style={styles.statValue}>{stats.crew}</strong>Członkowie załogi</div>
49 <div style={styles.stat}><strong style={styles.statValue}>{stats.online}</strong>Online</div>
50 </div>
51 </section>
52 );
53}
54
55// SERVER COMPONENT (symulacja): renderuje komponent kliencki jako dziecko
56// i przekazuje mu tylko serializowalne propsy (liczba polubień)
57function ServerPostsList({ showBundle }) {
58 const posts = use(queryServer('posts', 1200));
59
60 return (
61 <section style={{ ...styles.box, ...(showBundle ? styles.serverBox : null) }}>
62 <Tag type="server" show={showBundle} />
63 {posts.map(post => (
64 <article key={post.id} style={styles.post}>
65 <h3 style={styles.postTitle}>{post.title}</h3>
66 <div style={styles.postMeta}>
67 <span>Autor: {post.author}</span>
68 <LikeButton initialLikes={post.likes} showBundle={showBundle} />
69 </div>
70 </article>
71 ))}
72 </section>
73 );
74}
75
76// CLIENT COMPONENT: w projekcie osobny plik zaczynający się od 'use client'.
77// Tylko taki kod trafia do przeglądarki i może używać stanu oraz zdarzeń.
78function LikeButton({ initialLikes, showBundle }) {
79 const [liked, setLiked] = useState(false);
80 const likes = initialLikes + (liked ? 1 : 0);
81
82 return (
83 <span style={showBundle ? styles.clientBox : null}>
84 <Tag type="client" show={showBundle} />
85 <button onClick={() => setLiked(l => !l)} style={{ ...styles.like, color: liked ? '#ff6b6b' : '#8892b0' }}>
86 {liked ? 'Polubione' : 'Lubię to'} ({likes})
87 </button>
88 </span>
89 );
90}
91
92export default function App() {
93 const [showBundle, setShowBundle] = useState(false);
94
95 return (
96 <div style={styles.page}>
97 <div style={{ maxWidth: '700px', margin: '0 auto' }}>
98 <h1 style={styles.title}>React Server Components - symulacja</h1>
99 <p style={styles.subtitle}>
100 Komponenty serwerowe pobierają dane, a w przeglądarce działa tylko interaktywny przycisk polubienia
101 </p>
102 <button onClick={() => setShowBundle(s => !s)} style={styles.toggle}>
103 {showBundle ? 'Ukryj podział na serwer i klienta' : 'Pokaż, co trafia do przeglądarki'}
104 </button>
105
106 <Suspense fallback={<p style={styles.fallback}>Serwer liczy statystyki...</p>}>
107 <ServerStats showBundle={showBundle} />
108 </Suspense>
109
110 <h2 style={styles.h2}>Posty z bazy (Server Component + kliencki LikeButton)</h2>
111 <Suspense fallback={<p style={styles.fallback}>Serwer pobiera posty z bazy...</p>}>
112 <ServerPostsList showBundle={showBundle} />
113 </Suspense>
114 </div>
115 </div>
116 );
117}
118
119const styles = {
120 page: { minHeight: '100vh', background: 'linear-gradient(135deg, #0a0e27 0%, #1a1a3e 100%)', padding: '24px', color: '#e0e1dd', fontFamily: 'system-ui, sans-serif' },
121 title: { textAlign: 'center', color: '#00d4ff', margin: '0 0 6px', fontSize: '22px' },
122 subtitle: { textAlign: 'center', color: '#8892b0', margin: '0 0 14px', fontSize: '13px' },
123 toggle: { display: 'block', margin: '0 auto 16px', padding: '6px 14px', background: 'rgba(0,212,255,0.15)', color: '#00d4ff', border: '1px solid rgba(0,212,255,0.4)', borderRadius: '6px', cursor: 'pointer' },
124 fallback: { color: '#8892b0', padding: '16px', border: '1px dashed rgba(0,212,255,0.25)', borderRadius: '8px' },
125 box: { borderRadius: '10px', padding: '10px', marginBottom: '16px', border: '1px solid transparent' },
126 serverBox: { border: '1px dashed #ffa500', background: 'rgba(255,165,0,0.05)' },
127 clientBox: { display: 'inline-flex', flexDirection: 'column', alignItems: 'flex-end', gap: '2px', padding: '4px', border: '1px solid #00ff88', borderRadius: '6px' },
128 tag: { display: 'inline-block', fontSize: '10px', padding: '2px 6px', borderRadius: '4px', marginBottom: '6px' },
129 serverTag: { background: 'rgba(255,165,0,0.15)', color: '#ffa500' },
130 clientTag: { background: 'rgba(0,255,136,0.15)', color: '#00ff88', marginBottom: 0 },
131 statsGrid: { display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '10px' },
132 stat: { padding: '12px', background: 'rgba(0,212,255,0.08)', borderRadius: '8px', textAlign: 'center', fontSize: '12px', color: '#8892b0' },
133 statValue: { display: 'block', fontSize: '24px', color: '#00d4ff' },
134 h2: { color: '#64ffda', fontSize: '16px', margin: '0 0 8px' },
135 post: { padding: '10px 12px', margin: '6px 0', background: 'rgba(0,0,0,0.3)', borderRadius: '8px', border: '1px solid rgba(0,212,255,0.15)' },
136 postTitle: { color: '#00d4ff', margin: '0 0 6px', fontSize: '15px' },
137 postMeta: { display: 'flex', justifyContent: 'space-between', alignItems: 'center', fontSize: '12px', color: '#8892b0', gap: '8px' },
138 like: { background: 'none', border: 'none', cursor: 'pointer', fontSize: '12px' },
139};Widzisz błąd w tej lekcji?
Sprawdź się
Odpowiedz na pytania z tej lekcji. Wybierz odpowiedź, a od razu zobaczysz, czy jest poprawna.
1. Co oznacza dyrektywa 'use client' na początku pliku komponentu?
2. Jaka jest kluczowa różnica między React Server Components a tradycyjnym SSR?
To 2 z 5 pytań do tej lekcji. Pozostałe rozwiążesz w grze.
Zadania praktyczne w grze
- Klikanie w kolejności
Ułóż składnię Suspense boundary z komponentem fallback:
- Edytor kodu
Pokaż dane gwiazdozbioru z obserwatorium przez Suspense i use() z React 19. ___BLANK1___: getConstellation zapisuje w cache obietnicę z fetchConstellation(name), żeby każda nazwa była pobierana tylko raz. ___BLANK2___: ConstellationCard odczytuje dane przez use() z obietnicy zwróconej przez getConstellation(name) (nie twórz nowej obietnicy w renderze). ___BLANK3___: fallback granicy Suspense to element z tekstem o ładowaniu, np. <p>Ładowanie danych z teleskopu...</p>. Po kliknięciu innego gwiazdozbioru przez chwilę zobaczysz fallback, a po powrocie do już pobranego dane pojawią się od razu.
- Układanie w pionie
Uporządkuj etapy Streaming SSR w React (od wersji 18):
- Edytor kodu
Dokończ klasę MissionBoundary, granicę błędów dla paneli mostka. Czujnik paliwa jest uszkodzony i jego panel rzuca błąd, a pozostałe panele mają działać dalej. ___BLANK1___: getDerivedStateFromError zwraca nowy stan z polem error, w którym zapisujesz złapany błąd ({ error }). ___BLANK2___: componentDidCatch zgłasza awarię funkcją reportCrash(error, info.componentStack). ___BLANK3___: gdy w stanie jest błąd, render zwraca widok z this.props.renderFallback, któremu przekazujesz błąd ze stanu i funkcję this.reset (klasa zostaje bez JSX). Kliknięcie Restartuj czujnik naprawia czujnik i resetuje granicę, więc panel paliwa wraca z wartością 50%.
- Układanie w poziomie
Ułóż składnię opakowywania komponentu w Error Boundary z fallback:
- Układanie w pionie
Uporządkuj cykl życia żądania w architekturze React Server Components:
- Klikanie w kolejności
Uporządkuj implementację Server Component:
- Edytor kodu
Uzupełnij przyciski biblioteki komponentów Floty. ___BLANK1___: przycisk jest wyłączony, gdy ma prop disabled albo trwa ładowanie (loading). ___BLANK2___: padding pochodzi z tokenów: tokens.spacing dla wybranego rozmiaru size (sm, md albo lg). ___BLANK3___: IconButton pokazuje tylko ikonę, więc przekaż do Button atrybut aria-label z wartością label, żeby czytnik ekranu ogłosił nazwę przycisku. Komponenty nie mają stanu ani efektów, więc nadają się także do Server Components.