Пошук уроків, статей та іншого контенту
Розрізнятимете Server Components і Client Components та зрозумієте, де виконується кожен тип компонента.
У Next.js з App Router компоненти можуть бути двох типів:
Server Components — виконуються на сервері.
Client Components — виконуються на клієнті, тобто в браузері, коли потрібна інтерактивність.
За замовчуванням файли в каталозі app є Server Components. Щоб зробити компонент клієнтським, на початку файлу потрібно додати директиву "use client".
"use client";Ця директива має бути першим оператором у файлі, до імпортів.
Server Component виконується на сервері під час формування інтерфейсу. Такий компонент може:
виконувати асинхронні операції;
отримувати дані з бази даних або API;
використовувати секретні змінні середовища;
не передавати свій JavaScript-код у браузер;
формувати HTML та інші дані для клієнта.
Приклад Server Component:
// app/users/page.tsx
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 UsersPage() {
const users = await getUsers();
return (
<main>
<h1>Користувачі</h1>
<ul>
{users.map((user) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</main>
);
}Тут UsersPage не має директиви "use client", тому він є Server Component. Він може бути асинхронним і виконувати await getUsers() безпосередньо в тілі компонента.
Код завантаження даних виконується на сервері. Користувач отримує результат, але не отримує JavaScript цього компонента для виконання в браузері.
Client Component потрібен тоді, коли компонент використовує можливості браузера або інтерактивність:
обробники подій, наприклад onClick;
React Hooks, наприклад useState або useEffect;
API браузера, наприклад window, document, localStorage;
стан, який змінюється після взаємодії користувача.
Приклад Client Component:
"use client";
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
return (
<section>
<p>Лічильник: {count}</p>
<button type="button" onClick={() => setCount(count + 1)}>
Збільшити
</button>
</section>
);
}Без "use client" цей компонент не зможе використовувати useState та onClick.
Зазвичай Server Component відповідає за отримання даних, а невеликий Client Component — за взаємодію з користувачем.
// app/page.tsx
import Counter from "./Counter";
export default function HomePage() {
return (
<main>
<h1>Головна сторінка</h1>
<p>Цей текст належить Server Component.</p>
<Counter />
</main>
);
}// app/Counter.tsx
"use client";
import { useState } from "react";
export default function Counter() {
const [count, setCount] = useState(0);
return (
<section>
<p>Значення: {count}</p>
<button type="button" onClick={() => setCount((value) => value + 1)}>
Додати
</button>
</section>
);
}У цьому прикладі:
HomePage є Server Component, оскільки в ньому немає "use client".
Counter є Client Component.
Сервер формує сторінку та вставляє в неї початкову розмітку лічильника.
У браузері Counter підключає JavaScript і стає інтерактивним.
Натискання кнопки змінює стан уже в браузері.
Назва Client Component не означає, що він ніколи не виконується на сервері.
У Next.js Client Component може бути попередньо відрендерений на сервері під час першого завантаження сторінки. Це дає змогу швидше показати користувачу початкову HTML-розмітку.
Після цього браузер завантажує JavaScript і виконує гідрацію компонента. Гідрація — це процес підключення логіки React до вже створеної HTML-розмітки, після якого працюють події та стан.
Отже, для Client Component потрібно розрізняти:
початковий рендеринг — може виконуватися на сервері;
інтерактивність і подальші оновлення стану — виконуються в браузері.
Якщо код використовує window, document або localStorage, не слід звертатися до них під час рендерингу компонента, адже початковий рендеринг може відбутися на сервері.
"use client";
import { useEffect, useState } from "react";
export default function ThemeName() {
const [theme, setTheme] = useState("невідома");
useEffect(() => {
// Цей код виконується в браузері після монтування компонента
const savedTheme = window.localStorage.getItem("theme");
if (savedTheme) {
setTheme(savedTheme);
}
}, []);
return <p>Тема: {theme}</p>;
}Директива "use client" створює межу між серверною та клієнтською частинами дерева компонентів.
Якщо Server Component імпортує Client Component, це дозволено:
// app/page.tsx
import SearchBox from "./SearchBox";
export default function Page() {
return (
<main>
<h1>Пошук</h1>
<SearchBox />
</main>
);
}// app/SearchBox.tsx
"use client";
import { useState } from "react";
export default function SearchBox() {
const [query, setQuery] = useState("");
return (
<input
value={query}
onChange={(event) => setQuery(event.target.value)}
placeholder="Введіть запит"
/>
);
}Це поширений підхід: більша частина сторінки залишається серверною, а клієнтською стає лише інтерактивна частина.
Якщо файл позначений "use client", його імпорти та залежності входять до клієнтської частини програми.
Тому не варто без потреби додавати "use client" до великих компонентів або цілих сторінок. Краще залишити Server Component серверним і винести інтерактивну частину в окремий невеликий Client Component.
Server Component може передати Client Component дані через props.
// app/page.tsx
import ProductCard from "./ProductCard";
export default function Page() {
const product = {
id: 1,
name: "Механічна клавіатура",
price: 2500,
};
return (
<main>
<ProductCard product={product} />
</main>
);
}// app/ProductCard.tsx
"use client";
import { useState } from "react";
type Product = {
id: number;
name: string;
price: number;
};
type ProductCardProps = {
product: Product;
};
export default function ProductCard({ product }: ProductCardProps) {
const [isAdded, setIsAdded] = useState(false);
return (
<article>
<h2>{product.name}</h2>
<p>{product.price} грн</p>
<button type="button" onClick={() => setIsAdded(true)}>
{isAdded ? "Додано" : "Додати до кошика"}
</button>
</article>
);
}Props, які передаються через межу між сервером і клієнтом, мають бути серіалізованими. Зазвичай це прості значення:
рядки;
числа;
булеві значення;
масиви;
звичайні об'єкти з такими значеннями.
Функції обробників, наприклад onClick, не передаються із Server Component у Client Component як звичайні props. Обробник потрібно визначати всередині Client Component.
Server Component може передати розмітку дочірнім вмістом Client Component:
// app/page.tsx
import Panel from "./Panel";
import ServerMessage from "./ServerMessage";
export default function Page() {
return (
<main>
<Panel>
<ServerMessage />
</Panel>
</main>
);
}// app/Panel.tsx
"use client";
import type { ReactNode } from "react";
import { useState } from "react";
type PanelProps = {
children: ReactNode;
};
export default function Panel({ children }: PanelProps) {
const [isOpen, setIsOpen] = useState(true);
return (
<section>
<button type="button" onClick={() => setIsOpen((value) => !value)}>
{isOpen ? "Сховати" : "Показати"}
</button>
{isOpen && <div>{children}</div>}
</section>
);
}// app/ServerMessage.tsx
export default function ServerMessage() {
return <p>Цей текст сформований Server Component.</p>;
}У цьому випадку Panel керує станом у браузері, а ServerMessage залишається серверним. Такий підхід дає змогу додати інтерактивну оболонку, не перетворюючи весь її вміст на клієнтський код.
Починайте з Server Component і переходьте до Client Component лише за потреби.
Використовуйте Server Component, якщо компонент:
лише відображає дані;
отримує дані на сервері;
не має стану;
не обробляє події користувача;
не використовує API браузера.
Використовуйте Client Component, якщо компонент:
має useState, useEffect або інший React Hook, доступний на клієнті;
реагує на click, change, submit та інші події;
читає або змінює дані в браузері;
використовує window, document чи localStorage.
"use client" до кожного компонентаЦе позбавляє переваг Server Components і може збільшити кількість JavaScript, який потрібно завантажити браузеру.
Краще зробити клієнтською тільки інтерактивну частину:
// Серверна сторінка
import LikeButton from "./LikeButton";
export default function Article() {
return (
<article>
<h1>Заголовок статті</h1>
<p>Великий текст статті залишається серверним.</p>
<LikeButton />
</article>
);
}"use client"import { useState } from "react";
export default function Form() {
const [value, setValue] = useState("");
return <input value={value} onChange={(event) => setValue(event.target.value)} />;
}Такий компонент потрібно позначити директивою "use client".
window під час рендерингу"use client";
export default function Width() {
return <p>{window.innerWidth}</p>;
}Початковий рендеринг може відбуватися на сервері, де window не існує. Для доступу до API браузера використовуйте ефект або перевіряйте середовище в момент, коли код гарантовано виконується в браузері.
// Такий підхід не є звичайним способом передавання обробника
<ClientButton onClick={() => console.log("Натиснуто")} />Функція створена в Server Component не може бути передана Client Component як звичайне серіалізоване значення. Логіку обробки події слід розмістити в Client Component.
Client Component може брати участь у серверному початковому рендерингу. Тому код компонента має бути сумісним із цим сценарієм: не звертайтеся до браузерних глобальних об'єктів безпосередньо під час рендерингу.
У Next.js App Router компоненти за замовчуванням є Server Components.
Server Components виконуються на сервері та підходять для отримання даних і статичного відображення.
Client Components позначаються "use client".
Client Components потрібні для стану, обробників подій, React Hooks і API браузера.
Client Component може бути попередньо відрендерений на сервері, а потім гідрований у браузері.
Server Component може імпортувати Client Component і передавати йому серіалізовані props.
Щоб зменшити клієнтський JavaScript, робіть Client Component якомога меншим.
Найчастіше найкраща структура — серверна сторінка з окремими клієнтськими інтерактивними елементами.