Next.js zrewolucjonizował sposób tworzenia aplikacji React, oferując różne strategie renderowania, które można wybierać zależnie od charakterystyki strony i wymagań projektu. W tym module omówimy, jak strategie renderowania działają w kontekście App Router, wprowadzonego w Next.js 13 i rozwijanego w kolejnych wersjach.
Next.js przeszedł istotną ewolucję w podejściu do renderowania. W wersjach przed 13, używaliśmy specjalnych funkcji jak
getStaticProps, getServerSideProps czy getStaticPaths, aby określić strategię renderowania dla danej strony. Każda strona musiała wybrać jedną konkretną strategię.W App Router podejście jest inne - renderowanie jest teraz bardziej granularne i opiera się na koncepcji React Server Components. Zamiast określać strategię dla całej strony, możemy mieszać różne podejścia na poziomie poszczególnych komponentów.
U podstaw nowego modelu renderowania leżą React Server Components (RSC), które zmieniły sposób myślenia o aplikacjach React. Główne cechy RSC to:
W App Router wciąż możemy korzystać z tradycyjnych strategii renderowania, ale sposób ich implementacji jest inny. Przyjrzyjmy się każdej z nich.
W App Router, SSR jest domyślnym zachowaniem dla Route Handlers i Server Components.
1// app/products/page.tsx
2import { Product } from '@/components/Product';
3
4async function getData() {
5 // Ta funkcja uruchamia się dla każdego żądania na serwerze
6 const res = await fetch('https://api.example.com/products', { cache: 'no-store' });
7
8 if (!res.ok) {
9 throw new Error('Failed to fetch data');
10 }
11
12 return res.json();
13}
14
15export default async function ProductsPage() {
16 const products = await getData();
17
18 return (
19 <div>
20 <h1>Nasze produkty</h1>
21 <div className="grid">
22 {products.map(product => (
23 <Product key={product.id} data={product} />
24 ))}
25 </div>
26 </div>
27 );
28}Kluczowe cechy SSR w App Router:
cache: 'no-store' w funkcji fetch lub revalidate: 0, aby wskazać, że dane powinny być pobierane przy każdym żądaniuasync), co pozwala na bezpośrednie użycie await w ciele komponentuZalety SSR:
Wady SSR:
W App Router, SSG osiągamy poprzez domyślne zachowanie
fetch bez opcji cache: 'no-store'.1// app/blog/page.tsx
2import { BlogPost } from '@/components/BlogPost';
3
4async function getData() {
5 // Ta funkcja uruchamia się tylko w czasie budowania (lub na żądanie w trybie dev)
6 const res = await fetch('https://api.example.com/blog-posts');
7
8 if (!res.ok) {
9 throw new Error('Failed to fetch data');
10 }
11
12 return res.json();
13}
14
15export default async function BlogPage() {
16 const posts = await getData();
17
18 return (
19 <div>
20 <h1>Nasz blog</h1>
21 <div className="grid">
22 {posts.map(post => (
23 <BlogPost key={post.id} data={post} />
24 ))}
25 </div>
26 </div>
27 );
28}Kluczowe cechy SSG w App Router:
getStaticProps - wystarczy zwykły fetchZalety SSG:
Wady SSG:
W App Router, ISR osiągamy poprzez dodanie opcji
revalidate do funkcji fetch lub do segmentu trasy.1// app/products/[id]/page.tsx
2import { ProductDetails } from '@/components/ProductDetails';
3import { notFound } from 'next/navigation';
4
5// Opcja 1: Rewalidacja na poziomie strony
6export const revalidate = 3600; // Rewalidacja co godzinę
7
8async function getProduct(id) {
9 // Opcja 2: Rewalidacja na poziomie funkcji fetch
10 const res = await fetch(`https://api.example.com/products/${id}`, {
11 next: { revalidate: 3600 } // Również co godzinę
12 });
13
14 if (!res.ok) {
15 return null;
16 }
17
18 return res.json();
19}
20
21export default async function ProductPage({ params }) {
22 const product = await getProduct((await params).id);
23
24 if (!product) {
25 notFound();
26 }
27
28 return <ProductDetails product={product} />;
29}Kluczowe cechy ISR w App Router:
revalidatefetch za pomocą opcji next: { revalidate: seconds }Zalety ISR:
Wady ISR:
W App Router, CSR osiągamy poprzez oznaczenie komponentu jako "Client Component" przy użyciu dyrektywy
'use client' na początku pliku.1'use client';
2
3// app/dashboard/page.tsx
4import { useState, useEffect } from 'react';
5
6export default function Dashboard() {
7 const [data, setData] = useState(null);
8 const [isLoading, setIsLoading] = useState(true);
9
10 useEffect(() => {
11 async function fetchData() {
12 try {
13 const res = await fetch('/api/dashboard-data');
14 const jsonData = await res.json();
15 setData(jsonData);
16 } catch (error) {
17 console.error('Error fetching data:', error);
18 } finally {
19 setIsLoading(false);
20 }
21 }
22
23 fetchData();
24 }, []);
25
26 if (isLoading) return <div>Ładowanie danych...</div>;
27
28 return (
29 <div>
30 <h1>Panel kontrolny</h1>
31 {/* Renderowanie danych pobranych po stronie klienta */}
32 {data && data.map(item => (
33 <div key={item.id}>{item.name}</div>
34 ))}
35 </div>
36 );
37}Kluczowe cechy CSR w App Router:
'use client'useState, useEffect itp.)Zalety CSR:
Wady CSR:
Jedną z największych zalet App Router jest możliwość mieszania różnych strategii renderowania w ramach jednej aplikacji, a nawet jednej strony. Dzięki temu możemy wybrać optymalną strategię dla każdego komponentu.
Przykład wykorzystania mieszanych strategii:
1// app/products/[category]/page.tsx
2import { ProductList } from '@/components/ProductList';
3import { CategoryHeader } from '@/components/CategoryHeader';
4import FilterSidebar from '@/components/FilterSidebar'; // Client Component
5
6// Ta strona i jej dane są renderowane statycznie
7// z rewalidacją co godzinę
8export const revalidate = 3600;
9
10// Określamy, które kategorie mają być generowane statycznie
11export async function generateStaticParams() {
12 const categories = await fetch('https://api.example.com/categories').then(res => res.json());
13
14 return categories.map(category => ({
15 category: category.slug,
16 }));
17}
18
19async function getCategoryData(category) {
20 const categoryData = await fetch(`https://api.example.com/categories/${category}`);
21 return categoryData.json();
22}
23
24async function getProducts(category) {
25 const products = await fetch(`https://api.example.com/products?category=${category}`);
26 return products.json();
27}
28
29export default async function CategoryPage({ params }) {
30 // Pobieramy dane równolegle
31 const [categoryData, products] = await Promise.all([
32 getCategoryData((await params).category),
33 getProducts((await params).category),
34 ]);
35
36 return (
37 <div className="grid grid-cols-4 gap-4">
38 {/* Statycznie renderowany nagłówek kategorii */}
39 <CategoryHeader data={categoryData} />
40
41 {/* Interaktywny filtr renderowany po stronie klienta */}
42 <div className="col-span-1">
43 <FilterSidebar initialProducts={products} />
44 </div>
45
46 {/* Statycznie renderowana lista produktów */}
47 <div className="col-span-3">
48 <ProductList products={products} />
49 </div>
50 </div>
51 );
52}W tym przykładzie:
generateStaticParams)FilterSidebar jest Client Component, umożliwiający interaktywne filtrowanie po stronie klientaCategoryHeader i ProductList są Server Components renderowane statycznieWybór strategii renderowania zależy od charakterystyki danej części aplikacji:
Strony statyczne (dokumentacja, landing page, strony informacyjne):
Treści dynamiczne, ale nie wymagające najnowszych danych (blog, katalog produktów):
Treści wymagające najnowszych danych (ceny akcji, wiadomości, wyniki sportowe):
cache: 'no-store')Treści spersonalizowane i interaktywne elementy interfejsu (panele kontrolne, formularze):
'use client')Hybrydowe podejście (np. e-commerce):
W App Router, możemy również określić, które ścieżki dynamiczne powinny być generowane statycznie podczas budowania aplikacji:
1// app/blog/[slug]/page.tsx
2export async function generateStaticParams() {
3 const posts = await fetch('https://api.example.com/posts').then(res => res.json());
4
5 return posts.map((post) => ({
6 slug: post.slug,
7 }));
8}Dla tras, które nie są objęte przez
generateStaticParams:dynamicParams = false w opcjach segmentu, Next.js zwróci 404dynamicParams = true (domyślnie) lub nie jest ustawione, Next.js wygeneruje stronę na żądanie (SSR)Jeśli mamy wiele dynamicznych podstron (np. tysiące produktów w e-commerce), możemy zoptymalizować proces budowania poprzez:
1export async function generateStaticParams() {
2 // Pobierz tylko najpopularniejsze produkty
3 const popularProducts = await fetch('https://api.example.com/products/popular').then(res => res.json());
4
5 return popularProducts.map((product) => ({
6 id: product.id,
7 }));
8}1// app/api/revalidate/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3import { revalidatePath } from 'next/cache';
4
5export async function POST(request: NextRequest) {
6 const { path, secret } = await request.json();
7
8 // Sprawdzenie tajnego klucza dla bezpieczeństwa
9 if (secret !== process.env.REVALIDATION_SECRET) {
10 return NextResponse.json({ message: 'Invalid secret' }, { status: 401 });
11 }
12
13 if (!path) {
14 return NextResponse.json({ message: 'Path is required' }, { status: 400 });
15 }
16
17 try {
18 // Rewalidacja określonej ścieżki
19 revalidatePath(path);
20 return NextResponse.json({ revalidated: true, message: `Revalidated ${path}` });
21 } catch (err) {
22 return NextResponse.json({ message: 'Error revalidating' }, { status: 500 });
23 }
24}| Cecha | SSG | ISR | SSR | CSR | |-------|-----|-----|-----|-----| | Wydajność | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ | | SEO | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐ | | Aktualność danych | ⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | | Obciążenie serwera | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐ | | Interaktywność | ⭐⭐ | ⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐⭐ |
App Router w Next.js oferuje zaawansowane możliwości mieszania różnych strategii renderowania na poziomie komponentów, co pozwala na tworzenie wydajnych i elastycznych aplikacji. Kluczem do sukcesu jest wybór odpowiedniej strategii dla każdej części aplikacji, biorąc pod uwagę wymagania dotyczące wydajności, SEO i aktualności danych.
W następnym module omówimy bardziej szczegółowo mechanizmy rewalidacji danych w Next.js App Router, które są kluczowe dla implementacji ISR i zapewnienia aktualności treści.