Data Fetching в App Router
Next.js Data Fetching — система трубопроводів з паралельними потоками рідини різних кольорів
У попередньому уроці ми дізналися про Server та Client Components. Тепер зануримося глибше в отримання даних в App Router. Забудь про getStaticProps та getServerSideProps -- в App Router все набагато простіше: просто пишеш async компонент і робиш await fetch().
Async Server Components
Основний спосіб отримання даних в App Router -- асинхронні Server Components:
// src/app/posts/page.tsx
interface Post {
id: number;
title: string;
body: string;
}
export default async function PostsPage() {
const res = await fetch("https://jsonplaceholder.typicode.com/posts?_limit=10");
const posts: Post[] = await res.json();
return (
<div>
<h1>Статті</h1>
{posts.map((post) => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.body.slice(0, 100)}...</p>
</article>
))}
</div>
);
}
Ось і все! Ніяких useEffect, ніяких getStaticProps -- просто await прямо в компоненті.
Це працює, тому що Server Components виконуються на сервері. Браузер ніколи не бачить цей fetch -- він отримує вже готовий HTML з даними.
Кешування fetch
Next.js розширює стандартний fetch() додатковими можливостями кешування:
Статичні дані (кешуються назавжди)
// Дані кешуються під час збірки (аналог getStaticProps)
const res = await fetch("https://api.example.com/data", {
cache: "force-cache", // за замовчуванням
});
Динамічні дані (без кешу)
// Дані отримуються при кожному запиті (аналог getServerSideProps)
const res = await fetch("https://api.example.com/data", {
cache: "no-store",
});
Revalidation (ISR)
// Дані оновлюються кожні 60 секунд
const res = await fetch("https://api.example.com/data", {
next: { revalidate: 60 },
});
Порівняння
| Опція | Аналог Pages Router | Коли оновлюються дані |
|---|---|---|
cache: "force-cache" | getStaticProps | Під час збірки |
cache: "no-store" | getServerSideProps | При кожному запиті |
next: { revalidate: 60 } | ISR (revalidate) | Кожні 60 секунд |
Поведінка кешування за замовчуванням змінювалась у різних версіях Next.js. Завжди явно вказуй стратегію кешування, щоб уникнути сюрпризів.
Конфігурація на рівні сторінки
Замість налаштування кожного fetch, можна задати стратегію для всієї сторінки:
// src/app/dashboard/page.tsx
// Вся сторінка -- динамічна (SSR)
export const dynamic = "force-dynamic";
// Або: вся сторінка -- статична (SSG)
export const dynamic = "force-static";
// Revalidation для всієї сторінки
export const revalidate = 60;
Можливі значення dynamic:
"auto"-- Next.js вирішує сам (за замовчуванням)"force-dynamic"-- завжди SSR"force-static"-- завжди SSG"error"-- помилка, якщо сторінка використовує динамічні API
Parallel Data Fetching
Якщо компоненту потрібні дані з кількох джерел, роби запити паралельно:
// ❌ Погано: послідовні запити (повільно)
export default async function DashboardPage() {
const usersRes = await fetch("https://api.example.com/users");
const users = await usersRes.json();
const postsRes = await fetch("https://api.example.com/posts");
const posts = await postsRes.json();
// Загальний час = час(users) + час(posts)
return <div>...</div>;
}
// ✅ Добре: паралельні запити
export default async function DashboardPage() {
const [usersRes, postsRes] = await Promise.all([
fetch("https://api.example.com/users"),
fetch("https://api.example.com/posts"),
]);
const users = await usersRes.json();
const posts = await postsRes.json();
// Загальний час = max(час(users), час(posts))
return <div>...</div>;
}
Завжди використовуй Promise.all() для незалежних запитів. Якщо один запит займає 200ms, а другий 500ms, паралельний варіант економить 200ms.
Streaming з Suspense
Ще кращий підхід -- streaming, коли різні частини сторінки завантажуються незалежно:
// src/app/dashboard/page.tsx
import { Suspense } from "react";
import UserStats from "@/components/UserStats";
import RecentOrders from "@/components/RecentOrders";
import PopularProducts from "@/components/PopularProducts";
export default function DashboardPage() {
return (
<div>
<h1>Дашборд</h1>
<div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: "1rem" }}>
<Suspense fallback={<StatsSkeleton />}>
<UserStats />
</Suspense>
<Suspense fallback={<OrdersSkeleton />}>
<RecentOrders />
</Suspense>
</div>
<Suspense fallback={<ProductsSkeleton />}>
<PopularProducts />
</Suspense>
</div>
);
}
function StatsSkeleton() {
return <div className="skeleton" style={{ height: "200px" }}>Завантаження статистики...</div>;
}
function OrdersSkeleton() {
return <div className="skeleton" style={{ height: "200px" }}>Завантаження замовлень...</div>;
}
function ProductsSkeleton() {
return <div className="skeleton" style={{ height: "300px" }}>Завантаження товарів...</div>;
}
// src/components/UserStats.tsx -- Server Component
interface Stats {
totalUsers: number;
activeUsers: number;
newToday: number;
}
export default async function UserStats() {
// Цей запит може бути повільним -- інші секції не чекатимуть
const res = await fetch("https://api.example.com/stats", {
cache: "no-store",
});
const stats: Stats = await res.json();
return (
<div>
<h2>Статистика</h2>
<p>Всього: {stats.totalUsers}</p>
<p>Активних: {stats.activeUsers}</p>
<p>Нових сьогодні: {stats.newToday}</p>
</div>
);
}
loading.tsx -- автоматичний Suspense
Файл loading.tsx в директорії -- це автоматичний Suspense для всієї сторінки:
// src/app/dashboard/loading.tsx
export default function DashboardLoading() {
return (
<div>
<div className="skeleton" style={{ height: "2rem", width: "200px" }} />
<div className="skeleton" style={{ height: "400px", marginTop: "1rem" }} />
</div>
);
}
Server Actions
Server Actions -- це функції, які виконуються на сервері, але викликаються з клієнта. Ідеально для форм та мутацій:
// src/app/contact/page.tsx
import { revalidatePath } from "next/cache";
export default function ContactPage() {
async function submitForm(formData: FormData) {
"use server";
const name = formData.get("name") as string;
const email = formData.get("email") as string;
const message = formData.get("message") as string;
// Зберігаємо в БД (виконується на сервері!)
await saveToDatabase({ name, email, message });
// Оновлюємо кеш сторінки
revalidatePath("/contact");
}
return (
<form action={submitForm}>
<input name="name" placeholder="Ім'я" required />
<input name="email" type="email" placeholder="Email" required />
<textarea name="message" placeholder="Повідомлення" required />
<button type="submit">Надіслати</button>
</form>
);
}
async function saveToDatabase(data: { name: string; email: string; message: string }) {
// Тут буде збереження в БД
console.log("Збережено:", data);
}
Server Actions позначаються директивою "use server". Вони можуть бути визначені всередині Server Component або в окремому файлі з "use server" на початку.
Server Actions в окремому файлі
// src/app/actions.ts
"use server";
import { revalidatePath } from "next/cache";
export async function createPost(formData: FormData) {
const title = formData.get("title") as string;
const content = formData.get("content") as string;
// Збереження в БД
await savePost({ title, content });
revalidatePath("/posts");
}
async function savePost(data: { title: string; content: string }) {
console.log("Пост збережено:", data);
}
// src/components/CreatePostForm.tsx
"use client";
import { createPost } from "@/app/actions";
export default function CreatePostForm() {
return (
<form action={createPost}>
<input name="title" placeholder="Заголовок" />
<textarea name="content" placeholder="Контент" />
<button type="submit">Створити</button>
</form>
);
}
Revalidation -- оновлення кешу
Time-based (за часом)
// Оновлювати кожні 60 секунд
export const revalidate = 60;
On-demand (за запитом)
// src/app/actions.ts
"use server";
import { revalidatePath, revalidateTag } from "next/cache";
export async function publishPost() {
// ... зберігаємо пост
// Оновити конкретну сторінку
revalidatePath("/blog");
// Або оновити всі fetch з певним тегом
revalidateTag("posts");
}
Теги дозволяють точково оновлювати кеш:
const res = await fetch("https://api.example.com/posts", {
next: { tags: ["posts"] },
});
Підсумок
- В App Router дані отримуються через async Server Components -- просто
await fetch() - Кешування:
force-cache(SSG),no-store(SSR),revalidate(ISR) - Parallel fetching через
Promise.all()-- паралельні запити - Suspense + streaming -- поступове завантаження секцій
- Server Actions (
"use server") -- серверні функції для мутацій - Revalidation -- оновлення кешу за часом або за запитом
Що далі?
У наступному уроці вивчимо стилізацію в Next.js -- CSS Modules, Tailwind CSS, next/font та next/image для оптимізації.
Корисні посилання:
- Next.js: Data Fetching -- офіційна документація App Router
- Next.js: Server Actions -- мутації даних
- Next.js: Caching -- як працює кешування