Strona główna / Artykuły / Nowoczesne Next.js Mapped: Co zastępuje każda funkcja i kiedy ją używać

Nowoczesne Next.js Mapped: Co zastępuje każda funkcja i kiedy ją używać

Przewodnica po ośmiu funkcjonalnościach Next.js, od Server Components po Metadata API, pokazująca, jakie stare wzorce zastępują one oraz gdzie mogą sprawić problemy.

3113 słów

Next.js obejmuje teraz routowanie, pobieranie danych, cacheowanie, strategię renderowania oraz wiele aspektów API. Mimo to zespoły często go przyjmują, zachowując stare nawyki: pobieranie danych w funkcji useEffect, ręczne tworzenie tras API dla każdego formularza, reguły cacheowania, których nikt nie potrafi wyjaśnić. Ten przewodnik omawia osiem funkcjonalności, starsze wzorce, które one zastępują, oraz problemy występujące w rzeczywistych projektach, abyś mógł decydować element po elemencie, co powinno znaleźć się w twoim kodzie.

Dlaczego framework obejmuje teraz całą architekturę

Walmart, Nike, TikTok, OpenAI i Airbnb używają Next.js do uruchamiania aplikacji internetowych, a zaletą jest integracja: routowanie, pakietowanie, tryby renderowania, dostęp do danych i punkty końcowe serwera dzielą jeden repozytorium oraz jedną zbiór konwencji. Wybór routera i konfiguracja narzędzia do pakietowania nie stanowią już części procesu tworzenia projektu, dzięki czemu czas może być przeznaczony na rozwój produktu.

1. Komponenty serwerowe czynią z serwera domyślne miejsce renderowania

Komponenty serwerowe React (RSC) stanowią największą zmianę architektoniczną na tej liście. Komponent serwerowy jest wykonywany wyłącznie na serwerze i wysyła renderowany wynik do przeglądarki, dzięki czemu jego kod nigdy nie staje się częścią pliku JavaScript klienta.

Co znika z strony opartej na danych

W klasycznym React z renderowaniem po stronie klienta nawet komponent, który jedynie odczytuje dane i wyświetla listę, dodaje swój kod, logikę pobierania danych oraz zależności do pliku bundle, a następnie uruchamia się w przeglądarce. Z RSC komponent odczytuje dane na serwerze i wysyła tylko wynik. W App Routerze każdy komponent jest komponentem serwerowym, chyba że wyraźnie zaznaczysz inaczej, dlatego poniższy plik w ogóle nie wymaga żadnych instrukcji:

// app/products/page.tsx
// This component runs ONLY on the server. No "use client" directive needed.

Strona jest funkcją async, która bezpośrednio łączy się z bazą danych i zwraca kod strony. Nie ma żadnej trasy API pośredniczącej ani żadnego żądania po stronie klienta:

import { db } from "@/lib/db";export default async function ProductsPage() {
  // Direct database access. No API route. No fetch boilerplate.
  const products = await db.product.findMany({ take: 20 });  return (
    <main>
      <h1>Our Products</h1>
      <ul>
        {products.map((product) => (
          <li key={product.id}>
            <h2>{product.name}</h2>
            <p>${product.price}</p>
          </li>
        ))}
      </ul>
    </main>
  );
}

Brak useState, brak useEffect, brak flagi ładowania, brak wywołania fetch do własnego backendu. W przeglądarce uruchamia się mniej kodu, HTML przychodzi w pełnej formie (co jest korzystne dla motorów wyszukiwarki), a jest też mniej elementów do konserwacji. Ponieważ ten kod bezpośrednio łączy się z bazą danych, nigdy nie importuj go do pliku klienta; moduł danych dostępny tylko na serwerze wyraźnie określa tę granicę.

Gdzie nadal pasują komponenty klienta

Komponenty klienta są nadal potrzebne do wszystkiego, co interaktywne: obsługa zdarzeń, stan, efekty oraz API przeglądarki takie jak localStorage. Taki plik oznacza się, umieszczając na górze dyrektywę "use client":

// components/AddToCartButton.tsx
"use client";

Klawisz poniżej przechowuje fragment lokalnego stanu i reaguje na kliknięcia, co jest dokładnie tym rodzajem działania, które musi odbywać się w przeglądarce:

import { useState } from "react";export function AddToCartButton({ productId }: { productId: string }) {
  const [added, setAdded] = useState(false);  return (
    <button onClick={() => setAdded(true)}>
      {added ? "Added!" : "Add to Cart"}
    </button>
  );
}

Zacznij od komponentów serwerowych i zmieniaj je tylko tam, gdzie wymaga tego interaktywność, umieszczając "use client" jak najniżej w drzewie struktury: strona serwerowa z wbudowanym małym klawiszem klienta wymaga znacznie mniej JavaScriptu niż strona, która od samego początku jest komponentem klientowym. Aby lepiej zrozumieć, jak działa model renderowania w tle, zapoznaj się z architekturą stojącą za renderowaniem bez plików bundle.

2. Działania serwerowe zastępują ścieżki API do wykonywania mutacji

Akcje serwera umożliwiają napisanie funkcji, która jest wykonywana na serwerze, i jej bezpośrednie wywołanie z komponentu. Next.js generuje punkt końcowy HTTP, serializuje argumenty i zwraca wynik, dzięki czemu nie musisz już utrzymywać oddzielnego routingu tylko po to, by przyjąć dane z formularza.

Wzorzec dwufajlowy, który zostaje porzucony

Wcześniej mutacja wymagała obsługi w folderze api routera Pages, która odczytywała treść żądania, zapisywała dane do bazy danych i odpowiadała w formacie JSON:

// You needed an API route
// pages/api/create-post.ts
export default async function handler(req, res) {
  const { title, content } = req.body;
  await db.post.create({ data: { title, content } });
  res.status(200).json({ success: true });
}

Komponent musiał następnie ręcznie wywołać ten punkt końcowy, samodzielnie serializując treść żądania:

// Then in your component:
const response = await fetch("/api/create-post", {
  method: "POST",
  body: JSON.stringify({ title, content }),
});

Akcja zweryfikowana umieszczona obok formularza

Dzięki akcjom serwera strona importuje to, czego potrzebuje, w tym bibliotekę Zod do walidacji danych wejściowych:

// app/posts/new/page.tsx
import { redirect } from "next/navigation";
import { db } from "@/lib/db";
import { z } from "zod";

Działanie jest deklarowane obok formularza. Dyrektywa "use server" znajdująca się wewnątrz ciała funkcji oznacza je jako kod dostępny wyłącznie na serwerze; formularz przekazuje je do swojej właściwości action. Działanie to weryfikuje obiekt FormData za pomocą metody safeParse, zwraca błędy pól w przypadku nieudanej weryfikacji, a w przeciwnym razie zapisuje dane i przekierowuje użytkownika:

const schema = z.object({
  title: z.string().min(3, "Title must be at least 3 characters"),
  content: z.string().min(10, "Content is too short"),
});async function createPost(formData: FormData) {
  "use server";  const parsed = schema.safeParse({
    title: formData.get("title"),
    content: formData.get("content"),
  });  if (!parsed.success) {
    return { error: parsed.error.flatten().fieldErrors };
  }  await db.post.create({ data: parsed.data });
  redirect("/posts");
}export default function NewPostPage() {
  return (
    <form action={createPost}>
      <input name="title" placeholder="Post title" required />
      <textarea name="content" placeholder="Write something..." required />
      <button type="submit">Publish</button>
    </form>
  );
}

Weryfikacja jest niezbędna, ponieważ Server Action stanowi publiczny punkt końcowy, do którego każdy może uzyskać dostęp z dowolnymi danymi. Z tego samego powodu sprawdzenia uprawnień muszą znajdować się wewnątrz samego działania; artykuł dlaczego Server Actions wymagają uprawnień wewnątrz każdego ciała funkcji szczegółowo omawia ten temat. Należy również zauważyć, że nic tutaj nie odczytuje zwróconego obiektu błędu, więc w przypadku nieudanej weryfikacji nic się nie pokazuje. Następny wzorzec rozwiązuje ten problem.

Wyświetlanie błędów i stanu w oczekiwaniu za pomocą useActionState

Gdy formularz musi wyświetlać komunikaty walidacji lub wyłączyć przycisk, dopóki żądanie jest w trakcie przetwarzania, przenieś formularz do komponentu klienckiego i otocz działanie hookiem Reacta useActionState:

"use client";

Hook zwraca najnowszy stan wygenerowany przez działanie, otoczoną funkcję formAction do przekazania formularzowi oraz flagę isPending. Komponent wyświetla pierwszy błąd dla każdego pola i zmienia etykietę przycisku podczas przetwarzania zapytania:

import { useActionState } from "react";
import { createPost } from "./actions";export function PostForm() {
  const [state, formAction, isPending] = useActionState(createPost, null);  return (
    <form action={formAction}>
      <input name="title" placeholder="Post title" />
      {state?.error?.title && (
        <p className="text-red-500">{state.error.title[0]}</p>
      )}
      <textarea name="content" placeholder="Write something..." />
      {state?.error?.content && (
        <p className="text-red-500">{state.error.content[0]}</p>
      )}
      <button type="submit" disabled={isPending}>
        {isPending ? "Publishing..." : "Publish"}
      </button>
    </form>
  );
}

Detale, które często powodują zamieszanie: gdy akcja jest używana za pomocą useActionState, React wywołuje ją z poprzednim stanem jako pierwszym argumentem, a FormData jako drugim. Funkcja createPost pokazana wcześniej przyjmuje tylko formData, dlatego wersja eksportowana z ./actions dla tego hooka musi mieć sygnaturę (prevState, formData). Akcja ta musi również znajdować się w osobnym pliku z "use server" na górze, ponieważ komponent kliencki nie może definiować funkcji serwerowych bezpośrednio w kodzie.

3. Turbopack skraca cykl zwrotny w rozwoju

Turbopack osiągnął 100% skuteczności we wszystkich 8 298 testach integracyjnych z Next.js, a zespoły donoszą o czasie uruchamiania aplikacji od zera poniżej 3 sekund w projektach, które wcześniej wymagały ponad minuty. Jego status rozwojowy szybko się zmienia między wersjami, dlatego sprawdź aktualną dokumentację dotyczącą jego funkcjonowania w Twojej wersji, szczególnie jeśli chodzi o budowanie wersji produkcyjnych.

Aby włączyć tę funkcję podczas rozwoju, wystarczy ustawić odpowiedni flag w skrypcie dev:

// package.json
{
  "scripts": {
    "dev": "next dev --turbopack",
    "build": "next build"
  }
}

Nie jest potrzebna żadna dodatkowa konfiguracja. Turbopack oblicza treść stopniowo, odbudowując tylko to, co się zmieniło, dzięki czemu największe korzyści przynoszą duże projekty. Jeśli polegasz na niestandardowych ładowaczach lub pluginach webpack, najpierw sprawdź odpowiedniki w Turbopacku. Nasze porównanie narzędzi do pakowania omawia te kwestie.

4. Częściowe wstępne renderowanie łączy statyczną strukturę z danymi przesyłanymi strumieniowo

Częściowe wstępne renderowanie (PPR) natychmiast dostarcza już przygotowaną statyczną strukturę HTML, a następnie do niej przesyła dynamiczne elementy tej samej strony, wszystko w ramach jednej odpowiedzi.

Na stronie produktu układ, nawigacja i opis są takie same dla wszystkich; natomiast cena dostosowana do użytkownika, koszyk zakupów i liczba sztuk w magazynie różnią się. PPR wysyła natychmiast tę statyczną strukturę i uzupełnia elementy specyficzne dla każdej prośby w miarę ich przygotowywania.

Strona importuje jeden komponent statyczny oraz dwa dynamiczne:

// app/product/[id]/page.tsx
import { Suspense } from "react";
import { ProductDetails } from "./ProductDetails"; // static
import { PersonalizedPrice } from "./PersonalizedPrice"; // dynamic
import { StockStatus } from "./StockStatus"; // dynamic

Granica pomiędzy komponentami statycznymi a dynamicznymi jest wyznaczana za pomocą Suspense. Wszystko poza tą granicą może zostać wcześniej wyrenderowane; każdy element Suspense służy jako zastępca w strukturze, który jest zastępowany po zakończeniu renderowania jego potomków na serwerze:

export default function ProductPage({ params }: { params: { id: string } }) {
  return (
    <div>
      {/* This renders statically - instant */}
      <ProductDetails id={params.id} />      {/* These stream in dynamically */}
      <Suspense fallback={<div>Loading price...</div>}>
        <PersonalizedPrice productId={params.id} />
      </Suspense>      <Suspense fallback={<div>Checking stock...</div>}>
        <StockStatus productId={params.id} />
      </Suspense>
    </div>
  );
}

Należy zauważyć, że params jest tutaj definiowany jako zwykły obiekt. W nowszych wersjach Next.js params jest przekazywany jako Promise i musi zostać oczekiwany, dlatego należy dostosować jego definicję do używanej wersji.

PPR został wprowadzony przy użyciu flagi eksperymentalnej w konfiguracji Next.js, począwszy od importu typów:

// next.config.ts
import type { NextConfig } from "next";

a następnie sama flaga:

const nextConfig: NextConfig = {
  experimental: {
    ppr: true,
  },
};export default nextConfig;

W chwili pisania tego tekstu flaga ta została przearanżowana w nowszych wersjach (działanie PPR jest powiązane z ustawieniem Cache Components w Next.js 16), więc traktuj ten fragment jako ilustracyjny i sprawdź aktualną nazwę opcji. W każdym razie strona wydaje się statyczna, ponieważ jej szkielet jest zapisywany w pamięci cache, podczas gdy dane pozostają aktualne. Szerszy opis mechanizmów znajdziesz w artykule o częściowym pre-renderingu i równoczesnym renderowaniu.

5. Dyrektywa use cache sprawia, że katalogowanie w pamięci cache staje się jawne

Next.js 13 i 14 domyślnie katalogowały treści w pamięci cache bardzo agresywnie, przez co wiele zespołów odkrywało przestarzałe strony w środowisku produkcyjnym bez wyraźnej przyczyny. Next.js 16 przechodzi na model katalogowania w pamięci cache wybieralnego za pomocą Cache Components oraz dyrektywy use cache: określasz wtedy, co powinno być zapisywane, zamiast zgadywać, co już jest to.

Umieszczenie tej instrukcji na górze komponentu asynchronicznego zapamiętuje jego wyrenderowany wynik:

// A component that caches its output for 1 hour
async function PopularArticles() {
  "use cache";

Pozostała część komponentu pobiera i renderuje dane jak zwykle:

  const articles = await fetch("https://api.example.com/popular-articles").then(
    (r) => r.json()
  );  return (
    <ul>
      {articles.map((article: { id: string; title: string }) => (
        <li key={article.id}>{article.title}</li>
      ))}
    </ul>
  );
}

Komentarz wspomina o godzinie, ale sama instrukcja nie określa czasu trwania; okres ten pochodzi z profilu cache, ustawionego za pomocą cacheLife, a w przeciwnym razie stosuje się profil domyślny. Profile niestandardowe są deklarowane w konfiguracji:

// next.config.ts
const nextConfig = {
  experimental: {
    cacheLife: {
      "stale-for-a-day": {
        stale: 60 * 60, // 1 hour
        revalidate: 60 * 60 * 24, // 1 day
        expire: 60 * 60 * 24 * 7, // 1 week
      },
    },
  },
};

Te trzy wartości odpowiadają na różne pytania. stale określa, jak długo klient może korzystać ze swojej kopii bez sprawdzania serwera, revalidate wskazuje, jak często serwer odświeża wpis w tle, a expire to moment, po którym wpis jest usuwany i następna prośba musi czekać na świeże dane. To, czy cacheLife należy do kategorii experimental, zależy od Twojej wersji. Aby dowiedzieć się więcej o unieważnianiu na podstawie tagów, które będzie potrzebne po zmianach danych podczas zapisu, zapoznaj się z naszym przewodnikiem dotyczącym wykorzystania cache’u i odświeżania na podstawie tagów.

6. Funkcje streamingu AI z AI SDK

Vercel AI SDK integruje się z Route Handlers i hookami React, dzięki czemu strumieniowanie rozmów, wyszukiwanie z pomocą AI oraz generowane interfejsy użytkownika stają się standardowym kodem aplikacji.

Na serwerze obsługa tras imporтуje streamText oraz dostawcę modelu:

// app/api/chat/route.ts
import { streamText } from "ai";
import { openai } from "@ai-sdk/openai";

Obsługa żądania POST odczytuje rozmowę z ciała żądania, uruchamia transmisję danych i zwraca ją jako odpowiedź w formie strumienia (zamknięcie nawiasu funkcji jest przecięte w fragmencie kodu):

export async function POST(req: Request) {
  const { messages } = await req.json();  const result = streamText({
    model: openai("gpt-4o"),
    messages,
  });  return result.toDataStreamResponse();

Na stronie klienta strona czatu jest komponentem klienta, ponieważ przechowuje stan wprowadzanych danych:

// app/chat/page.tsx
"use client";

Hook useChat zarządza listą wiadomości, wartością wprowadzaną i jej wysyłką, a także ponownie renderuje stronę w miarę przychodzenia nowych tokenów:

import { useChat } from "ai/react";export default function ChatPage() {
  const { messages, input, handleInputChange, handleSubmit } = useChat();  return (
    <div>
      <div>
        {messages.map((m) => (
          <div key={m.id}>
            <strong>{m.role}:</strong> {m.content}
          </div>
        ))}
      </div>
      <form onSubmit={handleSubmit}>
        <input value={input} onChange={handleInputChange} placeholder="Ask anything..." />
        <button type="submit">Send</button>
      </form>
    </div>
  );
}

To oznacza około 30 linii kodu dla czatu strumieniowego, z obsługą tras na stronie backendowej oraz funkcją useChat na stronie frontendowej. Należy pamiętać, że API SDK AI szybko się rozwija: w nowszych głównych wersjach hook jest importowany z @ai-sdk/react, stan wejściowy musisz zarządzać sam, a pomocnik do generowania odpowiedzi ma inne nazwy. Zabezpiecz swoje wersje i sprawdź dokumentację SDK przed skopiowaniem tego kodu. Jeśli chodzi o interfejsy w stylu agenta z narzędziami i wieloma krokami, zapoznaj się z budową interfejsów UI agenta AI wieloetapowego przy użyciu Next.js i SDK AI.

7. Wzory App Router dla złożonych układów

App Router, wprowadzony w Next.js 13, jest obecnie standardowym sposobem tworzenia aplikacji Next.js. Dwie z jego funkcji zastępują potrzebę ręcznego zarządzania stanem.

Łączne ścieżki dla niezależnych paneli dashboardu

Łączne ścieżki renderują kilka stron w ramach jednego układu jednocześnie. Każda folder z prefiksem @ definiuje nazwane miejsce:

app/
  dashboard/
    @analytics/
      page.tsx
    @recent/
      page.tsx
    layout.tsx
    page.tsx

Układ otrzymuje każde takie miejsce jako właściwość obok children i umieszcza je w siatce:

// app/dashboard/layout.tsx
export default function DashboardLayout({
  children,
  analytics,
  recent,
}: {
  children: React.ReactNode;
  analytics: React.ReactNode;
  recent: React.ReactNode;
}) {
  return (
    <div className="grid grid-cols-3 gap-4">
      <div className="col-span-2">{children}</div>
      <aside>
        {analytics}
        {recent}
      </aside>
    </div>
  );
}

Ponieważ każde miejsce to odrębny segment ścieżki, ładuje własne dane i może mieć własne stany ładowania oraz błędów. Powolna zapytanie analityczne nie wpływa na działanie panelu z ostatnimi aktywnościami. Jeden z problemów: gdy przechodzisz do podścieżki, której miejsce nie definiuje, Next.js potrzebuje pliku default.tsx w tym miejscu, aby wiedzieć, co renderować przy ponownym załadowaniu.

Przechwytywanie ścieżek dla modali z rzeczywistymi adresami URL

Powszechny wzorzec interfejsu otwiera element w oknie modalnym, gdy adres URL zmienia się na ten element, aby można go było udostępnić. Obsługa tras realizuje to za pomocą konwencji folderowych:

app/
  photos/
    [id]/
      page.tsx      // Full page view at /photos/123
    (..)[id]/
      page.tsx      // Intercepted modal view
  page.tsx

Gdy użytkownik kliknie w tabeli, odpowiedni folder przechwytuje nawigację po stronie klienta i wyświetla wersję modalną. Gdy ktoś otworzy adres URL bezpośrednio lub odświeży stronę, wyświetla się zwykła pełna strona, bez żadnych ręcznych trików z historią przeglądania. Znaczniki (.), (..) i (...) odnoszą się do segmentów trasy, a nie do folderów systemu plików, przy czym okno modalne jest zazwyczaj wyświetlane za pomocą równoległego slotu @modal; dlatego sprawdź dokumentację dotyczącą routingu, aby dowiedzieć się o dokładnej strukturze wymaganej przez twoją architekturę.

8. Podstawy SEO: metadane i obrazy

API Metadata przekształca SEO w zwykły kod, który znajduje się obok strony, którą opisuje. Po imporcie typu Metadata:

// app/blog/[slug]/page.tsx
import type { Metadata } from "next";

strona eksportuje funkcję generateMetadata, która ładuje wpis i zwraca tytuł, opis, dane Open Graph oraz kartę Twittera:

export async function generateMetadata({
  params,
}: {
  params: { slug: string };
}): Promise<Metadata> {
  const post = await getPost(params.slug);  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      title: post.title,
      description: post.excerpt,
      images: [{ url: post.coverImage }],
      type: "article",
    },
    twitter: {
      card: "summary_large_image",
      title: post.title,
      description: post.excerpt,
      images: [post.coverImage],
    },
  };
}

Przeglądy w mediach społecznościowych, URL-ki kanoniczne oraz dane strukturyzowane pochodzą z tych samych danych, które jest renderowana strona. Jeśli komponent strony wywołuje również funkcję getPost, należy usunąć duplikaty zapytań (na przykład za pomocą cache w React), aby baza danych nie była pytaniana dwukrotnie.

Dla obrazów domyślnym rozwiązaniem powinien być next/image:

import Image from "next/image";

Domyślnie stosuje ładowanie opóźnione, serwuje warianty o odpowiedniej wielkości i konwertuje obrazy na format WebP lub AVIF, jeśli przeglądarka je obsługuje. Jasno określone wartości width i height zapewniają również odpowiedni rozmiar przestrzeni i zapobiegają zmianom w układzie strony:

export function ProductCard({ product }: { product: Product }) {
  return (
    <div>
      <Image
        src={product.imageUrl}
        alt={product.name}
        width={400}
        height={300}
        priority={false} // set true for above-the-fold images
      />
      <h2>{product.name}</h2>
    </div>
  );
}

Ustaw priority na true tylko dla obrazu widocznego powyżej linii foldingu, zwykle głównego obrazu lub obrazu produktu, aby przeglądarka mogła go wcześniej pobrać.

Rozsądna kolejność nauki

Każdy krok opiera się na poprzednim:

  1. Konwencje nazewania plików w App Router: layout.tsx, page.tsx, loading.tsx i error.tsx.
  2. Komponenty serwerowe oraz miejsce, gdzie znajduje się granica klienta.
  3. Akcje serwerowe z walidacją za pomocą Zod dla każdej modyfikacji.
  4. Funkcje Suspense i streaming do deklaratywnego zarządzania stanami ładowania.
  • Częściowe przedrenderowanie w modelu static-plus-dynamic.
  • Turbopack jest w fazie rozwoju, aby przyspieszyć proces iteracji.
  • Główne wnioski

    • Traktuj serwer jako domyślny środowisko wykonywania i stosuj "use client" aż do najmniejszych elementów interaktywnych.
    • Akcje serwera to punkty końcowe: waliduj dane wejściowe i sprawdzaj uprawnienia w każdej z nich.
    • Rysuj swoje granice statyczne i dynamiczne za pomocą Suspense; zarówno PPR, jak i cacheowanie opierają się na nich.
    • Wolę wyraźne cacheowanie za pomocą use cache i nazwanych profili cacheLife zamiast polegania na ustawieniach domyślnych.
    • Wiele z tych interfejsów zmieniło się pomiędzy ostatnimi wersjami, więc sprawdź flagi i sygnatury pod kątem wersji, którą faktycznie używasz.

    Niezależnie od tego, czy zaczynasz od nowa, czy migrujesz dużą aplikację do App Router, stopniowe wdrażanie tych rozwiązań to sposób o niskim ryzyku.

    Powiązane materiały