Pobieranie danych w App Routerze
App Router w Next.js opiera się na architekturze React Server Components (RSC), dzięki której dane można pobierać zarówno w komponentach serwerowych, jak i klienckich.
Pobieranie danych w komponentach serwerowych daje sporo korzyści, bo mają one bezpośredni dostęp do zasobów po stronie serwera, takich jak bazy danych czy system plików.
Takie podejście nie tylko wykorzystuje moc obliczeniową serwera i jego bliskość źródeł danych, co przyspiesza pobieranie danych i renderowanie, ale też odciąża stronę kliencką.
Załóżmy, że tak wygląda podstawowa architektura naszej aplikacji.
my-next-app/
├── app/
│ ├── layout.js
│ ├── page.js
│ ├── products/
│ │ ├── page.js
│ │ └── [id]/
│ │ └── page.js
│ ├── chat/
│ │ └── [chatId]/
│ │ └── page.js
├── components/
│ ├── ChatComponent.js
├── pages/
│ └── api/
│ ├── revalidate.js
│ └── send-message.js
├── package.json
└── next.config.jsKomponenty serwerowe
Architektura RSC w App Routerze pozwala używać słów kluczowych async i await w komponentach serwerowych. Wystarczy zdefiniować komponent jako funkcję asynchroniczną, żeby móc korzystać ze znanej z JavaScriptu składni await. To podstawa pobierania danych w komponentach serwerowych.
// app/product/page.tsx
// Define a TypeScript interface for the product data
interface Product {
id: number;
title: string;
price: number;
description: string;
category: string;
image: string;
}
// Next.js server component for the product page
export default async function ProductPage() {
// Fetch product details from the API
const res = await fetch('https://fakestoreapi.com/products/3', {
});
// Handle any fetch errors
if (!res.ok) {
throw new Error('Failed to fetch product details');
}
// Use the Product type to type the JSON response
const product: Product = await res.json();
return (
<div>
<h1>{product.title}</h1>
<img src={product.image} alt={product.title} width={200} />
<p>{product.description}</p>
<p>Price: ${product.price}</p>
<p>Category: {product.category}</p>
</div>
);
}
Komponent jest zdefiniowany jako funkcja async, więc dane można pobrać za pomocą await bezpośrednio w nim. Ta nowoczesna składnia upraszcza operacje asynchroniczne, a kod jest czystszy i czytelniejszy.
export default async function ProductPage() {
const res = await fetch('https://fakestoreapi.com/products/3');
const product: Product = await res.json();
// Render product details
}Komponent pobiera dane produktu z API za pomocą fetch. Dzięki SSR treść renderuje się na serwerze, co daje szybsze pierwsze ładowanie i lepsze SEO, bo do klienta trafia gotowy HTML.
🚩 Trzeba tylko pamiętać, że dane mogą zostać wygenerowane statycznie w czasie builda, a wtedy nie zaktualizują się same, dopóki nie przebudujesz aplikacji. Można temu zaradzić strategiami takimi jak Incremental Static Regeneration (ISR), które dbają o świeżość danych.
Cache'owanie
Domyślnie Next.js automatycznie zapisuje wartości zwracane przez fetch w Data Cache na serwerze. Oznacza to, że dane mogą zostać pobrane w czasie builda lub w czasie obsługi żądania, trafić do cache'a i być używane ponownie przy każdym kolejnym żądaniu danych.
// 'force-cache' is the default, and can be omitted
fetch('https://...', { cache: 'force-cache' })Odpowiedzi mogą dzięki temu przychodzić szybciej, bo dane są tymczasowo przechowywane i nie trzeba ich ponownie pobierać z serwera źródłowego. Źle skonfigurowane cache'owanie może jednak serwować nieaktualne dane.
Żeby jawnie sterować cache'owaniem, możesz użyć opcji takich jak 'no-store', która je wyłącza, albo 'force-cache', która wymusza zapisanie danych w cache'u:
/**
* By setting the cache option to 'no-store',
* we ensure that fresh data is fetched
* every time the page is requested.
*/
fetch('https://...', { cache: 'no-store' });Ustawienie cache na 'no-store' gwarantuje pobranie świeżych danych przy każdym żądaniu, co jest kluczowe w aplikacjach wymagających dokładnych danych w czasie rzeczywistym.
Rewalidacja (ISR)
Na produkcji dane pobrane podczas SSR mogą być statyczne i nie aktualizować się same. Rozwiązaniem jest Incremental Static Regeneration (ISR) w Next.js, który pozwala ponownie generować strony w tle, w miarę jak pojawia się ruch.
Ustawiając właściwość revalidate w obiekcie przekazywanym do funkcji pobierającej dane, określasz, jak często strona ma się odświeżać:
export default async function ProductPage() {
const res = await fetch('https://fakestoreapi.com/products/3',
{ next:
{
revalidate: 60
//re-fetching the latest data in 60 sec
}
});
const product: Product = await res.json();
// Render product details
}ISR łączy generowanie statyczne z dynamicznymi aktualizacjami, dzięki czemu użytkownicy dostają najnowszą treść bez przebudowywania całej strony.
Kiedy używać ISR, a kiedy no-store
- ISR sprawdza się przy treściach, które nie zmieniają się co sekundę, ale trzeba je co jakiś czas aktualizować: na blogach, stronach produktów czy w serwisach informacyjnych, gdzie dane są względnie stabilne.
no-storelepiej pasuje do treści, które muszą być aktualne w czasie rzeczywistym i nie mogą być nawet trochę nieświeże, takich jak wyniki meczów na żywo, notowania giełdowe czy dashboardy czasu rzeczywistego.
Webhooki i aktualizacja treści na żądanie
Oprócz Incremental Static Regeneration (ISR) i strategii cache'owania no-store mocną alternatywą przy zarządzaniu aktualizacjami treści w aplikacji Next.js są webhooki. Dzięki nim treść jest generowana statycznie i aktualizowana tylko na żądanie, co ogranicza zbędne obciążenie serwera i daje precyzyjną kontrolę nad tym, kiedy treść się odświeża.
Załóżmy, że korzystamy z headless CMS-a, takiego jak Contentful, Sanity czy WordPress, który po opublikowaniu nowej treści wysyła webhook do naszej aplikacji Next.js.
Webhook uruchomi rewalidację konkretnej strony, żeby wyświetlała najnowszą treść.
Utwórz w aplikacji Next.js route API, który obsłuży przychodzące żądanie webhooka. Zwykle znajdzie się on w katalogu pages/api projektu.
// pages/api/revalidate.ts
import { revalidatePath } from 'next/cache';
import type { NextRequest } from 'next/server';
import { NextResponse } from 'next/server';
export async function POST(request: NextRequest): Promise<NextResponse> {
try {
const { item } = await request.json();
const path = item?.get?.tree?.path;
if (path) {
/**
* revalidatePath is used to manually refresh the data and re-render
* the route segments under a specific path. This function updates
* the Data Cache and also invalidates the Full Route Cache.
*/
revalidatePath(path);
return NextResponse.json({ revalidated: true, now: Date.now() });
}
return NextResponse.json({
revalidated: false,
now: Date.now(),
message: 'Missing path to revalidate',
});
} catch (error) {
return NextResponse.json({ error: 'Error revalidating', details: error }, { status: 500 });
}
}
revalidatePath daje precyzyjną kontrolę nad aktualizacjami treści w aplikacji Next.js. Rewalidacja wybranych ścieżek na żądanie zwiększa elastyczność statycznych stron i sprawia, że użytkownicy mają dostęp do najnowszej treści z minimalnym opóźnieniem. To kluczowa funkcja w aplikacjach, w których świeżość treści ma duże znaczenie, a przy tym nie trzeba poświęcać wydajności.
Możemy też rewalidować wiele zasobów naraz za pomocą revalidateTag, przekazując do funkcji fetch tablicę tagów.
//revalidateTag refreshes all pages associated with the given tags.
revalidateTag('product-list');// app/products/page.ts
export default async function ProductListPage() {
const products = await fetch('https://.../products', {
next: {
revalidate: {
tags: ['product-list'], // Tag for the product list
},
},
})
return (
<div>
{/* product list */}
</div>
);
}// app/categories/[categoryId]/page.ts
export default async function CategoryPage({ params }) {
const { categoryId } = params;
const products = await fetch(`https://.../categories/${categoryId}/products`, {
next: {
revalidate: {
tags: ['product-list'],
},
},
})
return (
<div>
{/* render product list based on category */}
</div>
);
}Dzięki revalidateTag możesz sprawnie aktualizować zarówno listę produktów, jak i strony kategorii w odpowiedzi na zmiany w CMS-ie.
Webhooki dają precyzyjną kontrolę nad aktualizacjami treści: aplikacja dostarcza najświeższe informacje bez narzutu ciągłej rewalidacji po stronie serwera. To podejście godzi wydajność z elastycznością i idealnie pasuje do aplikacji, w których zmiany treści wynikają ze zdarzeń, a nie z upływu czasu. Wybierz webhooki, gdy chcesz ograniczyć obciążenie serwera i aktualizować treść tylko wtedy, gdy naprawdę ma to sens.
Komponenty klienckie
Renderowanie po stronie klienta (CSR) szczególnie dobrze sprawdza się w funkcjach wymagających częstych aktualizacji, takich jak czaty. Pobierając wiadomości po stronie klienta, możemy zbudować responsywny i interaktywny interfejs, w którym użytkownicy widzą wiadomości na bieżąco, w miarę ich wysyłania i odbierania.
'use client'; // Ensures this component runs on the client side
import React, { useEffect, useState } from 'react';
function ChatComponent({ chatId }) {
// State to hold the list of chat messages
const [messages, setMessages] = useState([]);
// State to manage the current input message
const [inputMessage, setInputMessage] = useState('');
// Fetch messages when the component mounts or when chatId changes
useEffect(() => {
const fetchMessages = async () => {
try {
const response = await fetch(`https://.../${chatId}/messages`);
if (!response.ok) {
throw new Error('Failed to fetch chat messages');
}
const data = await response.json();
setMessages(data);
} catch (err) {
// setting error
} finally {
//setting loading false
}
};
fetchMessages();
// Set up polling to refresh messages every 5 seconds
const interval = setInterval(fetchMessages, 5000);
return () => clearInterval(interval); // Clean up the interval on component unmount
}, [chatId]);
/** rest of code with
* handling new message input change func,
* message list and chat template
*/
}
export default ChatComponent;Zalety pobierania danych po stronie klienta w czacie
- Interakcja w czasie rzeczywistym: użytkownicy widzą nowe wiadomości na żywo, dzięki czemu czat jest responsywny i angażujący.
- Natychmiastowa informacja zwrotna: optymistyczny UI zapewnia płynne działanie, pokazując wiadomości od razu.
- Oszczędne wykorzystanie zasobów: obsługa aktualizacji po stronie klienta odciąża serwer, który może zająć się innymi operacjami, co poprawia ogólną wydajność.
Takie podejście wykorzystuje możliwości klienta, by zapewnić dynamiczny i interaktywny czat, i dobrze pokazuje siłę CSR we współczesnych aplikacjach webowych.
Podsumowanie
W tym wpisie pokrótce omówiłem pobieranie danych w Next.js, zarówno po stronie serwera, jak i klienta. Pokazałem, jak React Server Components (RSC) i Incremental Static Regeneration (ISR) optymalizują pobieranie danych po stronie serwera oraz jak rewalidacja na żądanie i renderowanie po stronie klienta poprawiają interaktywność i responsywność aplikacji webowych w czasie rzeczywistym.
Dziękuję za lekturę i mam nadzieję, że te wskazówki przydadzą Ci się przy budowaniu dynamicznych i wydajnych aplikacji w Next.js!