Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

React Server Components (RSC) - przyszłość React

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.

Czym są React Server Components?

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.

Różnice między RSC a SSR

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}

Podstawy Server Components

Server vs Client Components

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}

Data Fetching w Server Components

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}

Streaming i Suspense

Progresywne ładowanie contentu

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}

Kompozycja Server i Client Components

Patterns dla współpracy

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}

Zaawansowane patterns

Data Mutations w RSC

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}

Cache Strategies

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}

Performance i optymalizacje

Bundle splitting automatyczny

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}

Error Boundaries dla Server Components

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.

Przejdź do CodeWorlds