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

System komponentów - Client i Server Components

W Metropolii Quantum, architekci systemów kwantowych zawsze muszą podejmować fundamentalne decyzje: które procesy uruchomić na centralnych procesorach kwantowych (dla zadań wymagających najwyższej mocy obliczeniowej), a które na urządzeniach końcowych obywateli (dla natychmiastowej interaktywności). Ta dychotomia doskonale odzwierciedla rewolucyjną zmianę wprowadzoną przez Next.js 15 - podział na Server Components i Client Components.

Rewolucja React Server Components

React Server Components (RSC) stanowią największą zmianę paradygmatu w React od czasu wprowadzenia Hooks. Przed ich pojawieniem się, wszystkie komponenty React były renderowane na serwerze lub kliencie jako spójna całość. Teraz możemy tworzyć aplikacje, w których niektóre komponenty są renderowane wyłącznie na serwerze, inne na kliencie, a dane i struktury UI płynnie między nimi przepływają.

To jak różnica między tradycyjnymi systemami transportowymi Metropolii Quantum, gdzie pojazdy poruszają się tylko po określonych trasach, a nowym systemem kwantowego transportu, gdzie materia może być teleportowana bezpośrednio do miejsca docelowego.

Server Components - podstawy

W App Router Next.js 15, wszystkie komponenty są domyślnie Server Components, chyba że jawnie oznaczymy je jako Client Components.

Server Components są renderowane na serwerze i nie wysyłają żadnego JavaScript do przeglądarki. To jak kwantowe procesory obliczeniowe Metropolii Quantum, które wykonują skomplikowane obliczenia i przesyłają tylko końcowe wyniki do urządzeń obywateli.

Zalety Server Components:

  1. Dostęp do zasobów serwera - bezpośredni dostęp do baz danych, systemu plików i innych zasobów serwera
  2. Bezpieczeństwo - wrażliwe dane i logika pozostają na serwerze
  3. Automatyczny code splitting - tylko niezbędny kod jest wysyłany do klienta
  4. Mniejszy rozmiar paczek JavaScript - brak potrzeby przesyłania kodu do renderowania
  5. Lepsze SEO - szybsze renderowanie statyczne
  6. Lepsza wydajność pierwszego ładowania - mniejsza ilość JavaScript do pobrania i wykonania

Przykład Server Component:

1// app/dashboard/stats/page.tsx - Server Component (domyślnie)
2export default async function StatsPage() {
3  // Ten kod wykonuje się TYLKO na serwerze
4  const stats = await fetchQuantumSystemStats();
5  
6  return (
7    <section className="quantum-stats">
8      <h1>Statystyki Systemów Kwantowych</h1>
9      <div className="stats-grid">
10        {stats.map(stat => (
11          <StatCard key={stat.id} name={stat.name} value={stat.value} />
12        ))}
13      </div>
14    </section>
15  );
16}

Zwróć uwagę na kilka ważnych aspektów:

  1. Użycie async/await bezpośrednio w komponencie - to możliwe tylko w Server Components
  2. Brak użycia hooków React (useState, useEffect, itp.) - nie są one dostępne w Server Components
  3. Brak obsługi zdarzeń - Server Components nie mogą obsługiwać zdarzeń użytkownika

Client Components - podstawy

Client Components są renderowane w przeglądarce użytkownika i zapewniają interaktywność. Aby oznaczyć komponent jako Client Component, dodajemy na górze pliku dyrektywę

"use client"
.

To jak personalne urządzenia kwantowe obywateli Metropolii Quantum - lżejsze, ale natychmiastowo reagujące na akcje użytkownika.

Zalety Client Components:

  1. Interaktywność - obsługa zdarzeń użytkownika (kliknięcia, wprowadzanie tekstu, itp.)
  2. Hooki React - możliwość używania useState, useEffect, useContext i innych hooków
  3. Dostęp do API przeglądarki - window, document, localStorage, itp.
  4. Efekty po stronie klienta - animacje, interakcje w czasie rzeczywistym

Przykład Client Component:

1// app/dashboard/interactive-controls.tsx
2"use client"; // Ta dyrektywa oznacza komponent kliencki
3
4import { useState, useEffect } from 'react';
5
6export default function InteractiveControls() {
7  // Ten kod wykonuje się w przeglądarce użytkownika
8  const [activeSystem, setActiveSystem] = useState('energy');
9  const [status, setStatus] = useState('idle');
10  
11  useEffect(() => {
12    // Efekty działają tylko na kliencie
13    const timer = setTimeout(() => {
14      setStatus('ready');
15    }, 1000);
16    
17    return () => clearTimeout(timer);
18  }, []);
19  
20  return (
21    <div className="control-panel">
22      <h2>System: {activeSystem}</h2>
23      <p>Status: {status}</p>
24      <div className="button-group">
25        <button onClick={() => setActiveSystem('transport')}>
26          Transport
27        </button>
28        <button onClick={() => setActiveSystem('energy')}>
29          Energia
30        </button>
31        <button onClick={() => setActiveSystem('security')}>
32          Bezpieczeństwo
33        </button>
34      </div>
35    </div>
36  );
37}

Kluczowe aspekty:

  1. Dyrektywa
    "use client"
    na początku pliku
  2. Używanie hooków React (useState, useEffect)
  3. Obsługa zdarzeń (onClick)

Łączenie Server i Client Components

Najpotężniejszą cechą nowego modelu jest możliwość łączenia Server i Client Components w jednej aplikacji. Server Components mogą renderować Client Components, ale nie odwrotnie (z pewnymi wyjątkami przy użyciu wzorców). Jest to jak hybrydowy system transportu Metropolii Quantum, gdzie główne trasy są obsługiwane przez teleportację kwantową, a ostatni odcinek przez personalne pojazdy.

Przykład łączenia komponentów:

1// app/dashboard/page.tsx - Server Component
2export default async function DashboardPage() {
3  // Ten kod wykonuje się TYLKO na serwerze
4  const dashboardData = await fetchDashboardData();
5  
6  return (
7    <main className="quantum-dashboard">
8      <h1>Panel Kontrolny Metropolii Quantum</h1>
9      
10      {/* Server Component */}
11      <SystemStatusPanel systemsData={dashboardData.systems} />
12      
13      {/* Client Component (importowany i używany w Server Component) */}
14      <InteractiveControls />
15    </main>
16  );
17}
1// app/dashboard/system-status-panel.tsx - Server Component (domyślnie)
2export default function SystemStatusPanel({ systemsData }) {
3  return (
4    <div className="system-status-panel">
5      <h2>Status Systemów</h2>
6      <div className="status-grid">
7        {systemsData.map(system => (
8          <SystemStatusCard key={system.id} system={system} />
9        ))}
10      </div>
11    </div>
12  );
13}
1// app/dashboard/system-status-card.tsx - Server Component (domyślnie)
2import { StatusIndicator } from './status-indicator';
3
4export default function SystemStatusCard({ system }) {
5  return (
6    <div className="status-card">
7      <h3>{system.name}</h3>
8      <p>Wydajność: {system.performance}%</p>
9      
10      {/* Client Component używany w Server Component */}
11      <StatusIndicator status={system.status} />
12    </div>
13  );
14}
1// app/dashboard/status-indicator.tsx - Client Component
2"use client";
3
4import { useState, useEffect } from 'react';
5
6export function StatusIndicator({ status }) {
7  const [blinking, setBlinking] = useState(false);
8  
9  useEffect(() => {
10    const interval = setInterval(() => {
11      setBlinking(prev => !prev);
12    }, 500);
13    
14    return () => clearInterval(interval);
15  }, []);
16  
17  return (
18    <div className={`status-indicator ${status} ${blinking ? 'blink' : ''}`}>
19      {status === 'active' ? 'Aktywny' : 'Nieaktywny'}
20    </div>
21  );
22}

Strategie optymalizacji przy projektowaniu komponentów

Właściwe decydowanie, które komponenty powinny być Server Components, a które Client Components, jest kluczowe dla wydajności aplikacji. Oto kilka strategii, które pomogą Ci podejmować te decyzje:

1. Server Component jako domyślny wybór

Zawsze zaczynaj od Server Components, chyba że potrzebujesz funkcji dostępnych tylko w Client Components. To podobne do podejścia w Metropolii Quantum, gdzie domyślnie używa się centralnych jednostek obliczeniowych, chyba że potrzebna jest natychmiastowa interaktywność.

2. Granularna interaktywność

Zamiast oznaczać cały duży komponent jako "use client", wydziel tylko interaktywne części do osobnych Client Components, pozostawiając resztę jako Server Components.

Przed optymalizacją:

1// MNIEJ OPTYMALNE - cały komponent jest Client Component
2"use client";
3
4export default function ControlPanel() {
5  const [activeTab, setActiveTab] = useState('overview');
6  
7  return (
8    <div className="control-panel">
9      <h2>Panel Sterowania</h2>
10      <p>Ten panel pozwala na zarządzanie systemami kwantowymi.</p>
11      
12      <div className="tabs">
13        <button onClick={() => setActiveTab('overview')}>Przegląd</button>
14        <button onClick={() => setActiveTab('settings')}>Ustawienia</button>
15      </div>
16      
17      <div className="tab-content">
18        {activeTab === 'overview' ? (
19          <OverviewContent /> // Dużo statycznej zawartości
20        ) : (
21          <SettingsContent /> // Dużo statycznej zawartości
22        )}
23      </div>
24    </div>
25  );
26}

Po optymalizacji:

1// LEPSZE - rozdzielenie na Client i Server Components
2
3// control-panel.tsx - Server Component
4export default function ControlPanel() {
5  return (
6    <div className="control-panel">
7      <h2>Panel Sterowania</h2>
8      <p>Ten panel pozwala na zarządzanie systemami kwantowymi.</p>
9      
10      <TabController /> {/* Tylko ta część to Client Component */}
11    </div>
12  );
13}
14
15// tab-controller.tsx - Client Component
16"use client";
17import { useState } from 'react';
18import { OverviewContent } from './overview-content';
19import { SettingsContent } from './settings-content';
20
21export function TabController() {
22  const [activeTab, setActiveTab] = useState('overview');
23  
24  return (
25    <>
26      <div className="tabs">
27        <button onClick={() => setActiveTab('overview')}>Przegląd</button>
28        <button onClick={() => setActiveTab('settings')}>Ustawienia</button>
29      </div>
30      
31      <div className="tab-content">
32        {activeTab === 'overview' ? (
33          <OverviewContent /> // Importuje Server Component
34        ) : (
35          <SettingsContent /> // Importuje Server Component
36        )}
37      </div>
38    </>
39  );
40}

3. Wzorzec "Kontener i Prezentacja"

Użyj Server Components jako "kontenerów" do pobierania danych i organizacji, a Client Components jako "komponentów prezentacyjnych" odpowiedzialnych za interaktywność.

1// products-container.tsx - Server Component (kontener)
2export default async function ProductsContainer() {
3  const products = await fetchProducts();
4  
5  return <ProductsList products={products} />;
6}
7
8// products-list.tsx - Client Component (prezentacja)
9"use client";
10import { useState } from 'react';
11
12export function ProductsList({ products }) {
13  const [sortBy, setSortBy] = useState('name');
14  const sortedProducts = [...products].sort((a, b) => a[sortBy].localeCompare(b[sortBy]));
15  
16  return (
17    <div>
18      <div className="sorting">
19        <button onClick={() => setSortBy('name')}>Sortuj po nazwie</button>
20        <button onClick={() => setSortBy('price')}>Sortuj po cenie</button>
21      </div>
22      
23      <ul className="products">
24        {sortedProducts.map(product => (
25          <li key={product.id}>{product.name} - {product.price}</li>
26        ))}
27      </ul>
28    </div>
29  );
30}

Przepływ danych między Server i Client Components

Przesyłanie danych między serwerem a klientem wymaga specjalnego podejścia. W modelu RSC, dane płyną w jednym kierunku: z serwera do klienta. To jak jednokierunkowe kanały kwantowe w Metropolii Quantum - informacje płyną tylko z centrali do urządzeń osobistych.

Przekazywanie danych do Client Components:

1// server-component.tsx - Server Component
2async function ServerComponent() {
3  const data = await fetchSensitiveData();
4  
5  // Przekazanie przetworzonych danych do klienta
6  return <ClientComponent processedData={data.publicPart} />;
7}
8
9// client-component.tsx - Client Component
10"use client";
11
12export function ClientComponent({ processedData }) {
13  // Używanie danych przekazanych z serwera
14  return <div>{processedData.map(item => <p key={item.id}>{item.name}</p>)}</div>;
15}

Obsługa akcji użytkownika z Client Components:

Gdy Client Component musi wykonać akcję, która angażuje serwer, używamy React Server Actions lub API Routes.

Z użyciem Server Actions (nowa funkcja Next.js 15):

1// actions.js - Server Actions
2"use server";
3
4export async function updateSystemStatus(systemId, newStatus) {
5  // Ta funkcja działa na serwerze
6  await database.systems.update(systemId, { status: newStatus });
7  return { success: true };
8}
9
10// client-component.tsx - Client Component
11"use client";
12import { updateSystemStatus } from './actions';
13
14export function StatusToggle({ systemId, initialStatus }) {
15  const [status, setStatus] = useState(initialStatus);
16  
17  async function handleToggle() {
18    const newStatus = status === 'active' ? 'inactive' : 'active';
19    const result = await updateSystemStatus(systemId, newStatus);
20    
21    if (result.success) {
22      setStatus(newStatus);
23    }
24  }
25  
26  return (
27    <button onClick={handleToggle}>
28      {status === 'active' ? 'Dezaktywuj' : 'Aktywuj'}
29    </button>
30  );
31}

Różnice w przeładowaniach i nawigacji

W tradycyjnym modelu React, przeładowanie strony lub nawigacja często oznaczały ponowne uruchomienie całej aplikacji. W modelu React Server Components, nawigacja jest bardziej optymalna:

  • Server Components są ponownie renderowane na serwerze podczas nawigacji
  • Client Components zachowują swój stan podczas nawigacji w ramach tej samej aplikacji
  • Przejścia między stronami są płynniejsze dzięki mechanizmowi "instant navigation"

Jest to jak zaawansowane przełączniki kwantowe w Metropolii Quantum - pozwalają na natychmiastowe przejścia między różnymi układami bez przerywania działania systemów.

Typowe błędy i pułapki

1. Użycie hooków React w Server Components

1// ŹLE - Server Component próbuje używać hooków React
2export default function ServerComponent() {
3  // To spowoduje błąd!
4  const [count, setCount] = useState(0);
5  
6  return <div>Count: {count}</div>;
7}

2. Próba dostępu do API przeglądarki w Server Components

1// ŹLE - Server Component próbuje używać API przeglądarki
2export default function ServerComponent() {
3  // To spowoduje błąd!
4  const windowWidth = typeof window !== 'undefined' ? window.innerWidth : 0;
5  
6  return <div>Width: {windowWidth}px</div>;
7}

3. Przekazywanie funkcji do Server Components z Client Components

1// ŹLE - Client Component próbuje przekazać callback do Server Component
2"use client";
3
4export default function ClientComponent() {
5  const handleClick = () => console.log("Clicked!");
6  
7  // To nie zadziała! Server Components nie mogą przyjmować funkcji jako props
8  return <ServerComponent onClick={handleClick} />;
9}

Zaawansowane wzorce używania Server i Client Components

1. Wzorzec Providera

Jeśli potrzebujesz współdzielić stan między różnymi Client Components, używaj Client Providera na wysokim poziomie. Jest to podobne do centralnych jednostek synchronizacyjnych w Metropolii Quantum, które koordynują działania różnych urządzeń osobistych.

1// providers.tsx - Client Component
2"use client";
3import { createContext, useState } from 'react';
4
5export const ThemeContext = createContext({ theme: 'light', toggleTheme: () => {} });
6
7export function ThemeProvider({ children }) {
8  const [theme, setTheme] = useState('light');
9  const toggleTheme = () => setTheme(prev => prev === 'light' ? 'dark' : 'light');
10  
11  return (
12    <ThemeContext.Provider value={{ theme, toggleTheme }}>
13      {children}
14    </ThemeContext.Provider>
15  );
16}
1// layout.tsx - Server Component
2import { ThemeProvider } from './providers';
3
4export default function RootLayout({ children }) {
5  return (
6    <html>
7      <body>
8        <ThemeProvider>
9          {children}
10        </ThemeProvider>
11      </body>
12    </html>
13  );
14}

2. Wzorzec "podnoszenia stanu" (lifting state up)

Kiedy masz kilka Client Components, które potrzebują dostępu do tego samego stanu, podnieś stan do wspólnego przodka Client Component, a nie do Server Component.

1// parent.tsx - Server Component
2export default function ParentServerComponent() {
3  return <ClientParent />;
4}
5
6// client-parent.tsx - Client Component (wspólny przodek)
7"use client";
8import { useState } from 'react';
9
10export function ClientParent() {
11  const [sharedState, setSharedState] = useState('initial');
12  
13  return (
14    <div>
15      <ClientChildA sharedState={sharedState} setSharedState={setSharedState} />
16      <ClientChildB sharedState={sharedState} />
17    </div>
18  );
19}

3. Strategiczne użycie suspense

Użyj komponentu Suspense do zarządzania stanami ładowania w aplikacji hybrydowej:

1// page.tsx - Server Component
2import { Suspense } from 'react';
3
4export default function Page() {
5  return (
6    <div>
7      <h1>Dane systemowe Metropolii Quantum</h1>
8      
9      <Suspense fallback={<div>Ładowanie danych...</div>}>
10        <AsyncDataComponent />
11      </Suspense>
12    </div>
13  );
14}
15
16// async-data-component.tsx - Server Component z asynchronicznym kodem
17async function AsyncDataComponent() {
18  // Ten komponent może być zawieszony (suspendowany) podczas ładowania
19  const data = await fetchData();
20  
21  return <div>{/* Renderowanie danych */}</div>;
22}

Podsumowanie

System komponentów w Next.js 15 oparty na React Server Components to rewolucyjna zmiana, która łączy zalety renderowania serwerowego z interaktywnością kliencką. Podobnie jak w Metropolii Quantum, gdzie kwantowa teleportacja i osobiste pojazdy tworzą hybrydowy system transportu, tak w Next.js komponenty serwerowe i klienckie współpracują, tworząc wydajny, skalowalny i przyjazny dla użytkownika interfejs.

Przy projektowaniu aplikacji Next.js 15:

  1. Domyślnie używaj Server Components dla lepszej wydajności i bezpieczeństwa
  2. Przełącz na Client Components tylko wtedy, gdy potrzebujesz interaktywności, hooków lub API przeglądarki
  3. Rozdzielaj komponenty granularnie, aby zminimalizować ilość kodu JavaScript wysyłanego do klienta
  4. Pamiętaj o jednokierunkowym przepływie danych - z serwera do klienta
  5. Używaj Server Actions do przetwarzania formularzy i innych akcji użytkownika

Prawidłowe wykorzystanie Server i Client Components pozwoli Ci tworzyć aplikacje Next.js nowej generacji, które łączą szybkość ładowania, wydajność SEO i bogaty interfejs użytkownika - bez kompromisów, które były konieczne w tradycyjnych aplikacjach React.

W następnym module przejdziemy do praktyki i stworzymy naszą pierwszą aplikację "Hello World" z użyciem Next.js 15 i App Router, gdzie zastosujemy zdobytą wiedzę o Server i Client Components.

Przejdź do CodeWorlds