Пошук уроків, статей та іншого контенту
Виконуватимете запити в Client Components і визначатимете, коли клієнтський підхід є виправданим.
У Next.js Client Component може виконувати запити безпосередньо в браузері. Для цього компонент позначають директивою "use client" і запускають запит, наприклад, у useEffect.
Такий підхід відрізняється від отримання даних у Server Component:
запит виконується після завантаження JavaScript у браузері;
початково користувач може побачити стан завантаження;
дані можна повторно отримувати після дії користувача;
для запиту доступні браузерні API, зокрема localStorage, window та cookies;
URL запиту й отримані дані можуть бути видимими користувачу.
Client Component не означає, що всі його дані обов’язково потрібно завантажувати на клієнті. Клієнтський підхід варто використовувати тоді, коли він справді потрібен.
Компонент із клієнтською логікою починається з "use client":
"use client";
import { useEffect, useState } from "react";
type Post = {
id: number;
title: string;
body: string;
};
export default function Posts() {
const [posts, setPosts] = useState<Post[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
async function loadPosts() {
try {
const response = await fetch(
"https://jsonplaceholder.typicode.com/posts?userId=1"
);
if (!response.ok) {
throw new Error("Не вдалося завантажити дописи");
}
const data: Post[] = await response.json();
setPosts(data);
} catch (requestError) {
if (requestError instanceof Error) {
setError(requestError.message);
} else {
setError("Сталася невідома помилка");
}
} finally {
setIsLoading(false);
}
}
loadPosts();
}, []);
if (isLoading) {
return <p>Завантаження...</p>;
}
if (error) {
return <p role="alert">Помилка: {error}</p>;
}
return (
<ul>
{posts.map((post) => (
<li key={post.id}>
<h2>{post.title}</h2>
<p>{post.body}</p>
</li>
))}
</ul>
);
}Компонент можна використати на сторінці:
import Posts from "./Posts";
export default function HomePage() {
return (
<main>
<h1>Мої дописи</h1>
<Posts />
</main>
);
}У цьому прикладі:
posts зберігає отримані дані.
isLoading визначає, чи триває запит.
error зберігає повідомлення про помилку.
useEffect запускає запит після монтування компонента.
Порожній масив залежностей [] означає, що ефект запускається один раз для цього монтування.
Перевірка response.ok потрібна, оскільки fetch не вважає HTTP-відповіді на кшталт 404 або 500 помилкою JavaScript автоматично.
Часто запит потрібно повторювати, коли змінюється параметр: ідентифікатор користувача, пошуковий рядок або вибрана категорія.
У такому випадку параметр додають до масиву залежностей useEffect:
"use client";
import { useEffect, useState } from "react";
type Post = {
id: number;
title: string;
};
type UserPostsProps = {
userId: number;
};
export default function UserPosts({ userId }: UserPostsProps) {
const [posts, setPosts] = useState<Post[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const controller = new AbortController();
async function loadPosts() {
setIsLoading(true);
setError(null);
try {
const response = await fetch(
`https://jsonplaceholder.typicode.com/posts?userId=${userId}`,
{
signal: controller.signal,
}
);
if (!response.ok) {
throw new Error("Не вдалося завантажити дописи");
}
const data: Post[] = await response.json();
setPosts(data);
} catch (requestError) {
// Скасований запит не потрібно показувати як помилку
if (requestError instanceof DOMException) {
return;
}
if (requestError instanceof Error) {
setError(requestError.message);
} else {
setError("Сталася невідома помилка");
}
} finally {
// Компонент може бути демонтований до завершення запиту
if (!controller.signal.aborted) {
setIsLoading(false);
}
}
}
loadPosts();
return () => {
// Скасовуємо попередній запит перед новим запитом або демонтуванням
controller.abort();
};
}, [userId]);
if (isLoading) {
return <p>Завантаження дописів...</p>;
}
if (error) {
return <p role="alert">Помилка: {error}</p>;
}
return (
<ul>
{posts.map((post) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
);
}Якщо userId зміниться, React:
виконає функцію очищення попереднього ефекту;
скасує старий запит;
запустить ефект із новим userId.
Це запобігає ситуації, коли повільна стара відповідь перезаписує дані, отримані для нового параметра.
Клієнтське отримання даних доречне, коли дані:
залежать від дії користувача, наприклад натискання кнопки або зміни фільтра;
потрібно регулярно оновлювати після завантаження сторінки;
залежать від стану, який існує лише в браузері;
використовують localStorage, sessionStorage або інші браузерні API;
потрібні лише після взаємодії користувача, наприклад для відкриття модального вікна;
є персональними даними поточного клієнта й не потрібні для початкового HTML;
отримуються з WebSocket або іншого клієнтського джерела подій.
Наприклад, пошук із введенням тексту часто виконується на клієнті:
"use client";
import { useEffect, useState } from "react";
type Product = {
id: number;
title: string;
};
export default function ProductSearch() {
const [query, setQuery] = useState("");
const [products, setProducts] = useState<Product[]>([]);
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
if (!query.trim()) {
setProducts([]);
return;
}
const controller = new AbortController();
async function searchProducts() {
setIsLoading(true);
try {
const response = await fetch(
`https://dummyjson.com/products/search?q=${encodeURIComponent(query)}`,
{
signal: controller.signal,
}
);
if (!response.ok) {
throw new Error("Помилка пошуку");
}
const data: { products: Product[] } = await response.json();
setProducts(data.products);
} catch (error) {
// Скасування попереднього запиту є очікуваним під час нового введення
if (!(error instanceof DOMException)) {
console.error(error);
}
} finally {
if (!controller.signal.aborted) {
setIsLoading(false);
}
}
}
searchProducts();
return () => {
controller.abort();
};
}, [query]);
return (
<section>
<label>
Пошук:
<input
value={query}
onChange={(event) => setQuery(event.target.value)}
placeholder="Введіть назву товару"
/>
</label>
{isLoading && <p>Пошук...</p>}
<ul>
{products.map((product) => (
<li key={product.id}>{product.title}</li>
))}
</ul>
</section>
);
}У реальному застосунку для пошуку також часто додають затримку перед запитом, щоб не виконувати його після кожного символу. Проте основний принцип залишається тим самим: параметр пошуку є залежністю ефекту, а попередній запит скасовується під час нового.
Не варто без потреби переносити запити в Client Component, якщо дані:
потрібні для першого відображення сторінки;
мають бути доступними пошуковим системам у початковому HTML;
є загальними для всіх користувачів;
можуть бути отримані до відправлення сторінки браузеру;
містять секретні ключі або інші серверні облікові дані.
Клієнтський запит для таких даних може погіршити досвід користувача:
браузер спочатку завантажує JavaScript;
компонент показує стан завантаження;
лише потім виконується мережевий запит;
після відповіді з’являється основний вміст.
Якщо дані необхідні для початкового відображення, краще отримати їх на сервері, а Client Component залишити для інтерактивної частини. Наприклад, список можна передати в клієнтський компонент через props, а фільтрування або сортування виконувати вже в браузері.
Навіть простий клієнтський запит зазвичай має щонайменше три стани:
завантаження;
успішна відповідь;
помилка.
Іноді потрібен також окремий стан для порожнього результату:
if (isLoading) {
return <p>Завантаження...</p>;
}
if (error) {
return <p role="alert">{error}</p>;
}
if (posts.length === 0) {
return <p>Дописів не знайдено.</p>;
}Такий поділ робить інтерфейс зрозумілим для користувача та не залишає компонент без пояснення під час очікування відповіді.
useEffectНе слід виконувати fetch безпосередньо в тілі Client Component:
// Неправильно: запит виконуватиметься під час кожного рендерингу
const response = await fetch("/api/posts");Рендеринг має бути передбачуваним і не повинен запускати побічні ефекти. Для запитів, які виконуються після монтування або зміни залежностей, використовуйте useEffect.
Якщо запит використовує значення з props або state, це значення має бути в масиві залежностей:
useEffect(() => {
loadPosts(userId);
}, [userId]);Якщо залишити [], запит не повториться після зміни userId і компонент може показувати застарілі дані.
catchfetch переходить у catch переважно тоді, коли не вдалося виконати сам мережевий запит. HTTP-відповідь із кодом 404 або 500 потрібно перевіряти окремо:
const response = await fetch("/api/posts");
if (!response.ok) {
throw new Error("Сервер повернув помилку");
}Запит може завершитися після того, як компонент уже зник зі сторінки. Використовуйте AbortController, щоб скасувати непотрібний запит у функції очищення useEffect.
Якщо не показувати стан завантаження, користувач може не зрозуміти, чи виконується запит, чи сторінка зависла.
Директива "use client" збільшує обсяг JavaScript, який потрібно передати браузеру. Не додавайте її до компонента лише через те, що десь поруч потрібен клієнтський запит. Виносьте інтерактивну частину в окремий Client Component, якщо решта сторінки не потребує клієнтського виконання.
Для клієнтського запиту компонент позначають директивою "use client".
Запит зазвичай запускають у useEffect.
Дані, завантаження та помилки варто зберігати в окремих станах.
response.ok потрібно перевіряти вручну.
Значення, від яких залежить запит, додають до масиву залежностей.
AbortController допомагає скасувати застарілі або непотрібні запити.
Client Components виправдані для інтерактивних, персоналізованих і браузерно-залежних даних.
Дані для початкового відображення сторінки не слід без потреби завантажувати лише після монтування клієнтського компонента.