Вивчай

Data Fetching в App Router

Next.js Data Fetching — система трубопроводів з паралельними потоками рідини різних кольорів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 для оптимізації.

Інфо

Корисні посилання: