Strona główna / Artykuły / Gdzie narysować linię serwer-klient na stronie w Next.js App Router

Gdzie narysować linię serwer-klient na stronie w Next.js App Router

Praktyczny model mentalny dla komponentów serwerowych w React: co działa gdzie, jak strona posta na blogu dzieli się na części serwerową i kliencką oraz jakie zasady importu należy przestrzegać.

1321 słów

Komponenty serwerowe React często wydają się trudne w zrozumieniu, nawet po przeczytaniu dokumentacji, głównie dlatego, że wymagają porzucenia założenia, które każdy deweloper React stosował od lat: że komponenty działają w przeglądarce. Gdy to założenie znika, reszta przychodzi dość naturalnie. Ten artykuł buduje model mentalny wokół pojedynczej strony blogowej, pokazując, które elementy powinny znajdować się na serwerze, które wymagają interakcji z klientem oraz kilka zasad utrzymujących czystą granicę pomiędzy nimi.

Aby lepiej zrozumieć sam proces renderowania, zapoznaj się z architekturą renderowania bez plików bundle przy użyciu komponentów serwerowych.

Zanim pojawiły się komponenty serwerowe, każdy komponent React był w końcu uruchamiany w przeglądarce. Nawet przy renderowaniu po stronie serwera cały kod JavaScript dla całego drzewa komponentów był wysyłany do klienta i „hydratowany”, aby aplikacja mogła stać się interaktywna. Jest to marnotrawstwo zasobów w przypadku komponentów, które jedynie pobierają dane i przekazują je jako atrybuty – ich kod jest pobierany i uruchamiany, bez żadnej reakcji na działania użytkownika.

Komponent serwerowy zmienia tę sytuację, ponieważ jest uruchamiany wyłącznie na serwerze. Jego kod nigdy nie trafia do przeglądarki i nie jest „hydratowany”, więc nie dodaje żadnego kodu JavaScript do pliku klienta. Do przeglądarki trafia jedynie jego zrenderowany wynik, który React serializuje do kompaktowego pakietu razem z HTML.

Najprostszy sposób, aby odróżnić te dwa typy:

  • Komponent klienta: jest uruchamiany w przeglądarce (po uprzednim renderowaniu na serwerze), może przechowywać stan i reagować na zdarzenia.
  • Komponent serwerowy: działa na serwerze, może bezpośrednio odczytywać dane z baz danych lub plików i wysyłać tylko przetworzony wynik.
  • Dlaczego ta różnica się opłaca

    Weźmy typową stronę posta na blogu. Tytuł i treść pochodzą z bazy danych, wyglądają tak samo dla każdego odwiedzającego i ignorują kliknięcia. Tradycyjnie kod, który je renderuje, wraz z bibliotekami markdown lub formatowania, nadal trafia do przeglądarki. Dzięki komponentom serwerowym wszystko to pozostaje na serwerze. Pakiet maleje, a strony zazwyczaj ładują się szybciej, szczególnie na wolniejszych urządzeniach.

    Zespoły przechodzące na App Router, który jest zbudowany wokół komponentów serwerowych, zgłaszają lepsze wyniki Core Web Vitals, zwłaszcza LCP, w niektórych scenariuszach. Traktuj to jako zależne od kontekstu: korzyść zależy od tego, ile kodu JavaScript na stronie klienta można usunąć, więc zmierz wyniki swoich tras.

    Strona posta na blogu, podzielona na dwie części

    W projekcie Next.js App Router plik strony jest komponentem serwerowym, chyba że jest inaczej określone. Poniższa strona to funkcja async, która oczekuje na dane bezpośrednio z warstwy danych, obsługuje przypadek braku znalezienia elementu, renderuje treść statyczną, a następnie dodaje jeden interaktywny element do komentowania. Została napisana zgodnie z API Next.js 14:

    // app/posts/[slug]/page.tsx
    // This is a Server Component by default — no "use client" needed
    
    import { getPostBySlug } from "@/lib/db"
    import { PostContent } from "@/components/PostContent"
    import { CommentSection } from "@/components/CommentSection"
    
    type Props = {
      params: { slug: string }
    }
    
    export default async function PostPage({ params }: Props) {
      // Direct DB call — no useEffect, no API route, no loading state
      const post = await getPostBySlug(params.slug)
    
      if (!post) {
        return <div>Post not found.</div>
      }
    
      return (
        <article className="max-w-2xl mx-auto py-12 px-4">
          <h1 className="text-3xl font-bold mb-4">{post.title}</h1>
          <PostContent content={post.body} />
    
          {/* This one needs interactivity — so it's a Client Component */}
          <CommentSection postId={post.id} />
        </article>
      )
    }
    

    Zwróć uwagę na to, czego brakuje: nie ma useState, nie ma useEffect, brakuje otulacza ścieżki API oraz mechanizmu zarządzania stanem ładowania. Dane są oczekiwane dokładnie tak jak w każdej innej funkcji async, co stanowi istotę tego modelu.

    Dwie praktyczne uwagi. Od Next.js 15 params jest przekazywany jako Promise, więc strona powinna wykonać await params przed odczytaniem wartości slug; sprawdź wersję, na której działasz. A w przypadku rzeczywistego błędu braku strony, wywołanie funkcji notFound() z modułu next/navigation zwraca właściwy kod stanu 404, a nie zwykłą stronę z komunikatem o błędzie.

    Pole na komentarze jest inne. Zachowuje tekst wersji roboczej w pamięci stanu i reaguje na pisanie oraz kliknięcia, więc musi działać w przeglądarce. Dyrektywa "use client" umieszczona na górze pliku oznacza go jako komponent kliencki:

    // components/CommentSection.tsx
    "use client" // opts into browser rendering
    
    import { useState } from "react"
    
    type Props = {
      postId: string
    }
    
    export function CommentSection({ postId }: Props) {
      const [comment, setComment] = useState("")
    
      const handleSubmit = async () => {
        await fetch("/api/comments", {
          method: "POST",
          body: JSON.stringify({ postId, comment }),
        })
        setComment("")
      }
    
      return (
        <div className="mt-8">
          <textarea
            value={comment}
            onChange={(e) => setComment(e.target.value)}
            placeholder="Leave a comment..."
            className="w-full border rounded p-2 text-sm"
          />
          <button
            onClick={handleSubmit}
            className="mt-2 bg-blue-600 text-white px-4 py-2 rounded text-sm"
          >
            Post Comment
          </button>
        </div>
      )
    }
    

    Tutaj znajduje się wszystko, co jest interaktywne: lokalny stan pola tekstowego, obsługa zmian oraz obsługa wysyłki, która przesyła dane do ścieżki API, a następnie usuwa zawartość pola. Podczas rzeczywistego wdrażania należy do żądania dodać nagłówek Content-Type: application/json, a jako alternatywę dla oddzielnej ścieżki API rozważyć użycie Server Action.

    Taki podział jest łatwy do zrozumienia: serwer zarządza danymi, a klient – interakcją.

    Zasady utrzymywania czystości granic

    • W App Router komponenty są domyślnie Server Components.
    • Dodawaj "use client" tylko wtedy, gdy potrzebujesz stanu, efektów, obsługi zdarzeń lub API przeglądarki, takich jak window i localStorage.
    • Server Components mogą importować i renderować Client Components, tak jak to robi strona z CommentSection.
  • Komponenty klienckie nie mogą importować komponentów serwerowych, ale mogą je otrzymywać jako children lub inne atrybuty, co umożliwia umieszczenie treści renderowanych na serwerze wewnątrz interaktywnego interfejsu.
  • Atrybuty przekazywane z serwera do kliencka muszą być możliwe do serializacji: proste dane działają, natomiast funkcje i instancje klas nie.
  • Stylizacja pozostaje nienaruszona. Klasy Tailwind i CSS działają tak samo w obu typach komponentów.
  • "use client" oznacza również granicę, a nie pojedynczy plik: wszystko, co jest importowane z tego pliku, staje się częścią paczki klienckiej. Umieszczenie tej instrukcji jak najniżej w drzewie, na małych interaktywnych elementach, pozwala zachować resztę na serwerze.

    Podsumowanie

    Gdy tylko pojawi się pytanie „gdzie to się uruchamia?”, staje się ono pierwszym pytaniem dotyczącym komponentu, a domyślną odpowiedzią App Routera jest serwer.

    • Komponenty serwerowe przenoszą proces renderowania i dostęp do danych na serwer, więc nie zawierają żadnego JavaScriptu komponentowego.
    • Pobieranie danych odbywa się w formie zwykłego async/await wewnątrz komponentu.
    • Rozpatruj "use client" jako opcję dla interaktywnych elementów, a nie jako standard.
    • Pierwszy praktyczny krok: wybierz jeden komponent do pobierania danych w istniejącym projekcie, sprawdź, czy rzeczywiście potrzebuje przeglądarki, a jeśli nie – przekonwertuj go.

    Gdy taki model zostanie wdrożony, układy, streaming Suspense, Server Actions oraz równoległe trasy stają się znacznie łatwiejsze do opanowania, ponieważ wszystkie opierają się na tej samej zasadzie priorytetu serwera.

    Literatura pokrewna

  • 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.
  • SEO techniczne w Next.js App Router: Meta-dane, sitemapy i JSON-LD — Przeczytaj, w jaki sposób wspólne narzędzia do meta-danych, domyślne ustawienia layoutu, plik robots.ts, dynamiczny sitemap, poprawnie skonstruowane JSON-LD oraz audyty stron zapewniają aplikacji Next.js solidne podstawy SEO.
  • Dlaczego WebSockets zatrzymują się w ścieżkach API Next.js i jak stałowy serwer to naprawia — Dowiedz się, dlaczego serwer ws wewnątrz ścieżki pages/api nigdy nie kończy procedury handshake, oraz jak samodzielnie obsłużyć zdarzenie upgrade za pomocą stworzonego specjalnie serwera Next.js.