Poprawne rysowanie granicy „use client” w Next.js App Router
Dowiedz się, co tak naprawdę oznacza dyrektywa „use client”, dlaczego oznaczanie kontenerów powoduje zwiększenie rozmiaru plików, oraz jak komponenty podstawowe i sloty dziecięce utrzymują obliczenia serwera na samym serwerze.
Otwórz wiele kodów App Router i zobaczysz, że na górze niemal każdego komponentu znajduje się 'use client'. Ten nawyk szybko się utrwala: dodajesz onClick lub useState do rozwijanej listy, proces budowania aplikacji zgłasza błąd, a ta dyrektywa sprawia, że błąd znika. Powtórz to kilkadziesiąt razy i cała struktura trafia do pliku bundle przeglądarki, co oznacza aplikację jednostronicową z dodatkowymi procedurami. Ten przewodnik wyjaśnia, co tak naprawdę robi ta dyrektywa oraz jak ją umieścić, aby praca na serwerze pozostała na serwerze, a tylko te elementy, które są naprawdę interaktywne, wysyłały JavaScript.
Czym faktycznie oznacza ta dyrektywa
Nazwa sugeruje „wykonywaj to tylko w przeglądarce”, ale to nie jest jej znaczenie. 'use client' określa granicę modułu: plik, w którym kończy się graf struktur modułów serwera i zaczyna się pakiet klienta. Komponenty klienta są nadal renderowane do formatu HTML na serwerze; dodatkowo są wypełniane treścią w przeglądarce.
[ Server Component Tree ] (Executes solely on the server; zero KB client JS)
│
├── Server Component A (Fetches directly from DB)
│
▼
── [ 'use client' Boundary ] ──
│
├── Client Component B (Pre-rendered to HTML on server, hydrated on client)
│ │
│ └── Regular Component C (Now forced into the client bundle!)
Konsekwencja, która sprawia problemy zespołom, ma charakter transytywny. Każdy moduł importowany przez plik oznaczony jako 'use client' staje się również częścią paczki klienta, niezależnie od tego, czy sam zawiera tę dyrektywę, czy nie. Jeśli układ głównego panelu sterowania jest oznaczony jako moduł klienta ze względu na zawarte w nim spadkowe menu z profili, to jego pasek boczy, okna modalne, narzędzia pomocnicze oraz wszystkie ciężkie biblioteki, które importuje, trafiają również do przeglądarki. Aby lepiej zrozumieć, dlaczego kod działający wyłącznie na serwerze nie zajmuje żadnych bajtów w paczce, zapoznaj się z tym, jak React Server Components umożliwia renderowanie bez żadnej paczki.
Pułapka interaktywnego kontenera
Najczęstszym błędem jest przekształcanie całej strony w komponent kliencki tylko dlatego, że jedna mała jego część jest interaktywna. Poniższy przykład wymaga stanu tylko dla przełącznika filtrów, a mimo to cała karta sterująca jest oznaczona jako kod kliencki:
// ❌ BAD: The entire page is pulled into the client bundle
'use client';
import { useState, useEffect } from 'react';
import { db } from '@/lib/db'; // 🚨 Build error or massive security/bundle hazard!
export default function UserDashboard() {
const [isOpen, setIsOpen] = useState(false);
const [data, setData] = useState(null);
useEffect(() => {
// You've recreated client-side waterfall fetching
fetch('/api/user-data').then(res => res.json()).then(setData);
}, []);
return (
<div className="p-8">
<button onClick={() => setIsOpen(!isOpen)}>Toggle Filter</button>
{isOpen && <div className="dropdown">...</div>}
<div className="grid">
{/* Render heavy data tables */}
</div>
</div>
);
}
Powstają z tego dwa problemy. Importowanie klienta bazy danych do modułu klienckiego albo powoduje niepowodzenie kompilacji, albo, co gorsza, niesie ryzyko przeniesienia kodu i konfiguracji dostępnych tylko na serwerze do przeglądarki. Ponadto ładowanie danych odbywa się w funkcji useEffect, przez co strona renderuje się pusta, a następnie po hydratacji wysyła żądanie o dane, co powoduje ponowne utworzenie łańcucha operacji klienckich, których miały eliminować komponenty serwerowe. Dodanie pakietu server-only do modułów takich jak @/lib/db przekształca ten pierwszy błąd w wyraźny błąd kompilacji.
Promuj interaktywność na najniższy poziom
Komponent państwowy powinien znajdować się w najmniejszym komponencie, który go potrzebuje. W tym przypadku przycisk sterujący staje się samodzielnym modułem klienckim, a to, co ujawnia, jest przekazywane jako children:
// components/FilterToggle.tsx
'use client';
import { useState } from 'react';
export function FilterToggle({ children }: { children: React.ReactNode }) {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<button
onClick={() => setIsOpen(!isOpen)}
className="px-3 py-1.5 bg-neutral-100 rounded text-sm"
>
{isOpen ? 'Hide Filters' : 'Show Filters'}
</button>
{isOpen && <div className="mt-2">{children}</div>}
</div>
);
}
Strona pozostaje asynchronicznym komponentem serwerowym. Bezpośrednio zapytuje bazę danych, renderuje statyczną markup i wstawia przycisk sterujący tylko tam, gdzie ma miejsce interakcja:
// app/dashboard/page.tsx (Server Component by default)
import { db } from '@/lib/db';
import { FilterToggle } from '@/components/FilterToggle';
import { AnalyticsChart } from '@/components/AnalyticsChart';
export default async function UserDashboard() {
// Direct DB access — zero client-side fetch waterfalls
const metrics = await db.metrics.findMany();
return (
<main className="p-8">
<div className="flex justify-between items-center mb-6">
<h1 className="text-xl font-bold">Performance Analytics</h1>
<FilterToggle>
<p className="text-sm text-neutral-500">Filter options...</p>
</FilterToggle>
</div>
<div className="grid grid-cols-3 gap-4">
{/* AnalyticsChart can also remain an RSC if it doesn't need canvas/DOM APIs */}
<AnalyticsChart data={metrics} />
</div>
</main>
);
}
Absatz przekazany do FilterToggle jest renderowany na serwerze, mimo że znajduje się wewnątrz komponentu klienckiego. AnalyticsChart może pozostać komponentem serwerowym, o ile nie polega na API dostępnych tylko w przeglądarce, takich jak canvas; jeśli tak jest, to tylko ten wykres wymaga tej dyrektywy.
Komponowanie treści serwerowych wewnątrz obudów klienckich
Ta sama technika może być zastosowana do układów. Załóżmy komponent klienta CollapsibleShell, który zarządza stanem otwartym i zamkniętym, podobnie jak FilterToggle. Układ, będący komponentem serwerowym, tworzy kosztowny feed i przekazuje go do shella jako slot:
// app/layout.tsx
import { CollapsibleShell } from '@/components/CollapsibleShell';
import { ExpensiveServerFeed } from '@/components/ExpensiveServerFeed';
export default function RootLayout() {
return (
<html lang="en">
<body>
<CollapsibleShell>
{/* ExpensiveServerFeed runs on the server, streams HTML, and ships 0kb JS */}
<ExpensiveServerFeed />
</CollapsibleShell>
</body>
</html>
);
}
Ponieważ ExpensiveServerFeed jest tworzony w kontekście serwera i przekazywany jako children, jest renderowany w całości na serwerze. React wysyła jego zrenderowany wynik przez granicę w pakiecie RSC, a kod komponentu nigdy nie trafia do pliku klienta. Kluczowa różnica: moduł klienta, który importuje komponent, włącza go do pliku bundle, natomiast komponent klienta, który otrzymuje już zrenderowane elementy jako props, tego nie robi.
Lista kontrolna przed dodaniem dyrektywy
- Czy ten komponent wykorzystuje stan, efekty, obsługę zdarzeń lub API przeglądarki? Jeśli nie, pozostaw go na serwerze.
- Czy część interaktywna może zostać wyodrębniona do mniejszego komponentu potomnego?
- Czy treść renderowana na serwerze może zostać przekazana za pomocą
childrenlub innego atrybutu zamiast importowania? - Czy wszystkie atrybuty przechodzące przez granicę są serializowalne, bez funkcji lub instancji klas z wyjątkiem Server Actions?
- Czy oznaczenie tego pliku spowodowałoby zaimportowanie ciężkiej biblioteki, takiej jak parser Markdown lub narzędzia do obsługi dat, które mogłyby pozostać na serwerze?
Główne wnioski
- Komponenty serwerowe są standardem z powodu konkretnych przyczyn; przechowuj tam pobieranie danych, narzędzia do parsowania oraz statyczną markę.
- Izoluj małe, interaktywne elementy zamiast konwertować ich kontenery.
children oraz innych właściwości slotów, aby otoczyć wynik z serwera zachowaniem klienta, nie wysyłając go.'use client' jako celową granicę architektoniczną, a nie sposób na ukrycie błędu podczas budowania aplikacji. Gdy zastosuje się to prawidłowo, pliki pakietowe pozostają małe, a treść pojawia się od razu w odpowiednim miejscu.Literatura pokrewna
- Budowanie odpornej strony z detalami filmu za pomocą Next.js App Router — Dowiedz się, jak poprawnie pobierać i przechowywać w pamięci cache dane z API OMDB w Next.js App Router przy użyciu asynchronicznych komponentów serwerowych, parametrów oczekiwanych oraz prawidłowego obsługi błędu 404.
- Migracja z next/router: Parametry, płytkie adresy URL i błędy 404 w App Router — Dowiedz się, jak parametry, funkcja useParams, aktualizacje płytkich adresów URL, nawigacja programowa oraz prawdziwe strony 404 funkcjonują w Next.js App Router po usunięciu next/router.
- Pobieranie danych w trybie paralelnym w React: Suspense z Promise.all i allSettled — Naucz się, jak usunąć sekwencyjne żądania za pomocą Promise.all, zapobiec awariom stron z powodu danych opcjonalnych dzięki Promise.allSettled oraz wyświetlać stany ładowania za pomocą Suspense.