Пошук уроків, статей та іншого контенту
Ознайомитеся з моделлю отримання даних у Next.js та вибором місця для виконання запитів.
Data Fetching — це отримання даних із джерела, наприклад:
бази даних;
зовнішнього API;
внутрішнього сервісу;
файлу або іншого сховища.
У Next.js важливо не лише знати, як виконати запит, а й розуміти, де саме він має виконуватися:
на сервері;
у браузері;
через спеціальний серверний маршрут.
У цьому уроці розглядається модель App Router, у якій компоненти сервера є стандартним способом отримання даних.
Файли в каталозі app за замовчуванням є Server Components. Вони виконуються на сервері, тому можуть отримувати дані до того, як HTML буде надіслано браузеру.
Серверний компонент може бути асинхронним:
type User = {
id: number;
name: string;
email: string;
};
async function getUsers(): Promise<User[]> {
const response = await fetch(
'https://jsonplaceholder.typicode.com/users',
{
next: { revalidate: 60 },
}
);
if (!response.ok) {
throw new Error('Не вдалося завантажити користувачів');
}
return response.json();
}
export default async function HomePage() {
const users = await getUsers();
return (
<main>
<h1>Користувачі</h1>
<ul>
{users.map((user) => (
<li key={user.id}>
{user.name} — {user.email}
</li>
))}
</ul>
</main>
);
}Цей код можна розмістити у файлі app/page.tsx нового Next.js-проєкту.
Послідовність виконання така:
Next.js запускає HomePage на сервері.
Сервер виконує fetch.
Отримані дані використовуються для побудови HTML.
Готовий результат надсилається браузеру.
Браузер не виконує функцію getUsers і не бачить її код.
Зазвичай логіку отримання даних виносять в окрему функцію:
async function getUsers() {
const response = await fetch('https://example.com/api/users');
if (!response.ok) {
throw new Error('Помилка запиту');
}
return response.json();
}Таку функцію можна викликати безпосередньо із серверного компонента:
export default async function UsersPage() {
const users = await getUsers();
return <UsersList users={users} />;
}Це допомагає:
відокремити отримання даних від розмітки;
повторно використовувати запити;
легше обробляти помилки;
простіше тестувати код.
Якщо запит використовується лише на одній сторінці, його також можна залишити в цьому файлі. Окрему функцію варто створювати тоді, коли запит має власну логіку або використовується в кількох місцях.
дані потрібні для першого відображення сторінки;
запит використовує секретний ключ;
потрібно звернутися до бази даних;
не потрібно реагувати на дії користувача без перезавантаження;
важливо не передавати зайві дані або службову логіку в браузер.
Наприклад, серверний компонент може працювати з секретним змінним середовища:
async function getProducts() {
const response = await fetch('https://api.example.com/products', {
headers: {
Authorization: `Bearer ${process.env.API_TOKEN}`,
},
});
if (!response.ok) {
throw new Error('Не вдалося завантажити товари');
}
return response.json();
}Секретний токен не має потрапити до браузера. Саме тому такий код повинен виконуватися на сервері.
запит запускається після натискання кнопки;
дані залежать від введення користувача;
потрібно оновити частину сторінки без навігації;
потрібна постійна взаємодія з користувачем.
Для цього створюють Client Component за допомогою директиви 'use client'.
'use client';
import { useEffect, useState } from 'react';
type User = {
id: number;
name: string;
};
export default function UsersWidget() {
const [users, setUsers] = useState<User[]>([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
async function loadUsers() {
try {
const response = await fetch(
'https://jsonplaceholder.typicode.com/users'
);
if (!response.ok) {
throw new Error('Помилка запиту');
}
const data: User[] = await response.json();
setUsers(data);
} finally {
setIsLoading(false);
}
}
loadUsers();
}, []);
if (isLoading) {
return <p>Завантаження...</p>;
}
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}У цьому прикладі запит виконується вже в браузері. Компонент має доступ до стану, подій і React Hooks, але його код надсилається клієнту.
Часто сторінка отримує основні дані на сервері, а інтерактивна частина відображається клієнтським компонентом.
Серверний компонент:
import UsersList from './UsersList';
type User = {
id: number;
name: string;
};
async function getUsers(): Promise<User[]> {
const response = await fetch(
'https://jsonplaceholder.typicode.com/users'
);
if (!response.ok) {
throw new Error('Не вдалося завантажити користувачів');
}
return response.json();
}
export default async function Page() {
const users = await getUsers();
return <UsersList users={users} />;
}Клієнтський компонент app/UsersList.tsx:
'use client';
type User = {
id: number;
name: string;
};
type UsersListProps = {
users: User[];
};
export default function UsersList({ users }: UsersListProps) {
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}У такій архітектурі:
сервер завантажує дані;
сервер передає їх як props;
клієнтський компонент додає інтерактивність.
Це часто краще, ніж одразу завантажувати всі дані в браузері.
Якщо серверний компонент має доступ до бази даних, зазвичай не потрібно створювати додатковий HTTP-запит до власного API.
Наприклад, замість такої схеми:
Server Component → власний HTTP API → база данихможна використати:
Server Component → база данихТак менше проміжних кроків і немає зайвого мережевого запиту.
Умовний приклад:
import { getUsersFromDatabase } from '@/lib/users';
export default async function Page() {
const users = await getUsersFromDatabase();
return (
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
);
}Функція доступу до бази даних повинна залишатися серверною. Її не можна імпортувати в Client Component.
Для fetch можна вказати, як часто потрібно оновлювати результат.
const response = await fetch('https://example.com/api/products', {
next: { revalidate: 60 },
});У цьому прикладі Next.js може повторно використовувати результат протягом 60 секунд, а потім оновити його.
Такий варіант підходить для даних, які:
змінюються не щосекунди;
можуть бути трохи неактуальними;
однакові для багатьох відвідувачів.
const response = await fetch('https://example.com/api/orders', {
cache: 'no-store',
});cache: 'no-store' означає, що запит потрібно виконувати заново, не використовуючи кешований результат.
Це доречно для:
поточного стану замовлення;
персональних даних;
адміністративної інформації;
даних, які мають бути актуальними під час кожного запиту.
Вибір режиму залежить від вимог до актуальності даних, а не лише від зручності написання коду.
Під час виконання серверного запиту користувачеві може знадобитися показати стан завантаження. Для цього в каталозі маршруту можна створити файл loading.tsx:
export default function Loading() {
return <p>Завантаження сторінки...</p>;
}Наприклад, для сторінки app/users/page.tsx файл app/users/loading.tsx показуватиметься, поки сторінка завантажує дані.
Для помилок можна створити error.tsx:
'use client';
type ErrorPageProps = {
error: Error & { digest?: string };
reset: () => void;
};
export default function ErrorPage({
error,
reset,
}: ErrorPageProps) {
return (
<main>
<h1>Щось пішло не так</h1>
<p>{error.message}</p>
<button onClick={() => reset()}>
Спробувати ще раз
</button>
</main>
);
}Компонент помилки має бути клієнтським, оскільки кнопка використовує обробник події.
Не слід додавати приватні ключі до коду з 'use client'. Цей код може потрапити до браузера.
Запити із секретами виконуйте в Server Component або іншому серверному коді.
useEffect для початкових даних без потребиЯкщо дані потрібні одразу для відображення сторінки, у App Router зазвичай простіше отримати їх у Server Component.
useEffect варто використовувати для клієнтської взаємодії, а не як універсальний спосіб отримання всіх даних.
response.okfetch не вважає HTTP-відповідь зі статусом 404 або 500 винятком автоматично. Тому потрібно перевіряти відповідь вручну:
const response = await fetch(url);
if (!response.ok) {
throw new Error('Помилка HTTP-запиту');
}Якщо клієнтському компоненту потрібні лише ім’я та ідентифікатор, не передавайте йому весь об’єкт із внутрішніми полями.
Передавайте через props лише ті дані, які справді потрібні для роботи компонента.
Серверному компоненту часто не потрібно викликати власний Route Handler через HTTP. Спільну логіку краще винести в серверну функцію та використовувати її безпосередньо.
Перед написанням запиту поставте собі такі запитання:
Чи потрібні дані для першого відображення сторінки?
Так — почніть із Server Component.
Чи містить запит секрети або звертається до бази даних?
Так — виконуйте його на сервері.
Чи запускається запит після дії користувача?
Так — розгляньте Client Component.
Чи потрібні дані кожного разу актуальні?
Так — використайте відповідний режим без кешування.
Чи можна дозволити короткочасне повторне використання результату?
Так — задайте період повторної перевірки через revalidate.
У App Router компоненти є серверними за замовчуванням.
Запити для початкового вмісту сторінки зазвичай виконують на сервері.
Client Components потрібні для стану, подій і запитів, що запускаються в браузері.
Серверний код може безпечно працювати із секретами та базою даних.
Дані із Server Component можна передати в Client Component через props.
Для fetch потрібно свідомо визначати політику кешування та актуальність даних.
Перевіряйте response.ok і передбачайте стани завантаження та помилок.