React Server Components to rewolucyjna funkcjonalność React, która pozwala na renderowanie komponentów po stronie serwera z zachowaniem interaktywności po stronie klienta. To fundamentalna zmiana w sposobie myślenia o aplikacjach React.
React Server Components to komponenty, które są renderowane po stronie serwera i wysyłają do klienta już gotowy HTML wraz z instrukcjami dla React, jak zintegrować ten HTML z interaktywnymi komponentami klienta.
1// Traditional SSR - cała aplikacja renderowana na serwerze
2function TraditionalSSRPage() {
3 const [count, setCount] = useState(0);
4 const posts = await fetchPosts(); // Cała logika musi być na kliencie po hydratacji
5
6 return (
7 <div>
8 <PostsList posts={posts} />
9 <Counter count={count} setCount={setCount} />
10 </div>
11 );
12}
13
14// React Server Components - mix serwera i klienta
15async function ServerComponentPage() {
16 // Ten kod działa TYLKO na serwerze
17 const posts = await fetchPosts(); // Bezpośrednie wywołanie bazy danych
18 const user = await getUser();
19
20 return (
21 <div>
22 {/* Server Component - renderowane na serwerze */}
23 <PostsList posts={posts} />
24
25 {/* Client Component - interaktywny na kliencie */}
26 <Counter />
27
28 {/* Server Component z danymi z serwera */}
29 <UserProfile user={user} />
30 </div>
31 );
32}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}1// utils/api.js (server-side only)
2import { cache } from 'react';
3
4// Automatyczne cache'owanie dla tej samej requesta
5export const getPosts = cache(async () => {
6 const response = await fetch('https://api.example.com/posts', {
7 // Server Components mogą używać secrets bezpiecznie
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 // Parallel data fetching
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 {/* Nested Server Component z własnym data fetching */}
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}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ący się content */}
10 <Suspense fallback={<SkeletonCard />}>
11 <UserStats />
12 </Suspense>
13
14 {/* Wolniej ładujący się content */}
15 <Suspense fallback={<SkeletonChart />}>
16 <AnalyticsChart />
17 </Suspense>
18
19 {/* Bardzo wolno ładujący się content */}
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 - complex aggregations
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}1// patterns/CompositionPatterns.jsx
2
3// ✅ DOBRE - Server Component może renderować Client Component
4function 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// Usage:
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 - Użycie render props pattern
52'use client';
53function GoodClientComponent({ renderContent }) {
54 const [show, setShow] = useState(false);
55
56 return (
57 <div>
58 <button onClick={() => setShow(!show)}>Toggle</button>
59 {show && renderContent()}
60 </div>
61 );
62}
63
64// Usage:
65function ParentComponent() {
66 return (
67 <GoodClientComponent
68 renderContent={() => <ServerContent />}
69 />
70 );
71}1// actions/postActions.js
2'use server'; // Server Actions
3
4import { revalidatePath } from 'next/cache';
5
6export async function createPost(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 // Revalidate cache dla 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 { useFormStatus } from 'react-dom';
47
48function SubmitButton() {
49 const { pending } = useFormStatus();
50
51 return (
52 <button type="submit" disabled={pending}>
53 {pending ? 'Creating...' : 'Create Post'}
54 </button>
55 );
56}
57
58export default function CreatePostForm() {
59 const [state, formAction] = useActionState(createPost, null);
60
61 return (
62 <form action={formAction}>
63 <div>
64 <label htmlFor="title">Title:</label>
65 <input
66 type="text"
67 id="title"
68 name="title"
69 required
70 />
71 </div>
72
73 <div>
74 <label htmlFor="content">Content:</label>
75 <textarea
76 id="content"
77 name="content"
78 required
79 />
80 </div>
81
82 {state?.error && (
83 <div className="error">{state.error}</div>
84 )}
85
86 {state?.success && (
87 <div className="success">Post created successfully!</div>
88 )}
89
90 <SubmitButton />
91 </form>
92 );
93}1// utils/cache.js
2import { unstable_cache } from 'next/cache';
3
4// Custom cache wrapper z tagami
5export function createCachedFunction(fn, keyParts, options = {}) {
6 return unstable_cache(
7 fn,
8 keyParts,
9 {
10 revalidate: 3600, // 1 hour default
11 tags: options.tags || [],
12 ...options
13 }
14 );
15}
16
17// Cached functions
18export const getCachedPosts = createCachedFunction(
19 async () => {
20 return await db.post.findMany({
21 include: { author: true },
22 orderBy: { createdAt: 'desc' }
23 });
24 },
25 ['posts'],
26 {
27 tags: ['posts'],
28 revalidate: 300 // 5 minutes
29 }
30);
31
32export const getCachedUser = createCachedFunction(
33 async (userId) => {
34 return await db.user.findUnique({
35 where: { id: userId },
36 include: { profile: true }
37 });
38 },
39 ['user'],
40 {
41 tags: ['users'],
42 revalidate: 3600 // 1 hour
43 }
44);
45
46// components/PostsWithCache.jsx
47export default async function PostsWithCache() {
48 // Automatyczne cache'owanie
49 const posts = await getCachedPosts();
50
51 return (
52 <div>
53 <h1>Cached Posts</h1>
54 {posts.map(post => (
55 <article key={post.id}>
56 <h2>{post.title}</h2>
57 <p>By {post.author.name}</p>
58 <CacheRevalidateButton postId={post.id} />
59 </article>
60 ))}
61 </div>
62 );
63}
64
65// actions/cacheActions.js
66'use server';
67
68import { revalidateTag } from 'next/cache';
69
70export async function revalidatePosts() {
71 revalidateTag('posts');
72}
73
74export async function revalidateUsers() {
75 revalidateTag('users');
76}1// Server Components nie trafiają do client bundle!
2
3// ServerHeavyComponent.jsx (Server Component)
4import { someHeavyLibrary } from 'heavy-library'; // Nie trafi do bundle 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}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 // Send to monitoring service
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// Usage w layout
44export default function Layout({ children }) {
45 return (
46 <html>
47 <body>
48 <ServerErrorBoundary>
49 {children}
50 </ServerErrorBoundary>
51 </body>
52 </html>
53 );
54}React Server Components fundamentalnie zmieniają sposób myślenia o aplikacjach React, oferując lepszą wydajność, automatyczny code splitting i bezpieczniejsze operacje po stronie serwera.