Пошук уроків, статей та іншого контенту
Передавайте props і серіалізовані дані між Server та Client Components, дотримуючись обмежень межі клієнта.
У Next.js з App Router компоненти за замовчуванням є Server Components. Вони виконуються на сервері й можуть:
отримувати дані з бази даних або API;
працювати з секретами та змінними середовища;
формувати HTML і передавати результат браузеру.
Client Components виконуються в браузері. Вони потрібні для:
обробників подій;
useState, useEffect та інших клієнтських хуків;
доступу до API браузера;
інтерактивної поведінки.
Client Component позначається директивою "use client" на початку файлу:
"use client";Ця директива створює межу клієнта. Компонент, позначений "use client", і його імпорти потрапляють до клієнтської частини застосунку.
Server Component може імпортувати Client Component:
// Server Component
import ProductList from "./ProductList";Але Client Component не повинен імпортувати Server Component напряму.
Найпоширеніший сценарій — Server Component отримує дані, а потім передає їх Client Component через props.
Розглянемо сторінку зі списком товарів.
Файл app/products/page.tsx:
import ProductList from "@/components/ProductList";
type Product = {
id: number;
name: string;
price: number;
createdAt: string;
};
async function getProducts(): Promise<Product[]> {
const products = [
{
id: 1,
name: "Механічна клавіатура",
price: 2499,
createdAt: new Date("2026-01-15"),
},
{
id: 2,
name: "USB-мікрофон",
price: 3199,
createdAt: new Date("2026-02-03"),
},
];
return products.map((product) => ({
...product,
// Перетворюємо Date на рядок перед передаванням у Client Component
createdAt: product.createdAt.toISOString(),
}));
}
export default async function ProductsPage() {
const products = await getProducts();
return (
<main>
<h1>Товари</h1>
<ProductList products={products} />
</main>
);
}Цей компонент виконується на сервері. Він отримує дані, приводить їх до потрібного формату й передає масив products у ProductList.
Файл components/ProductList.tsx:
"use client";
import { useState } from "react";
type Product = {
id: number;
name: string;
price: number;
createdAt: string;
};
type ProductListProps = {
products: Product[];
};
export default function ProductList({ products }: ProductListProps) {
const [query, setQuery] = useState("");
const filteredProducts = products.filter((product) =>
product.name.toLowerCase().includes(query.toLowerCase()),
);
return (
<section>
<label htmlFor="product-search">Пошук:</label>
<input
id="product-search"
type="search"
value={query}
onChange={(event) => setQuery(event.target.value)}
placeholder="Введіть назву товару"
/>
<ul>
{filteredProducts.map((product) => (
<li key={product.id}>
<strong>{product.name}</strong>
<div>Ціна: {product.price.toFixed(2)} грн</div>
<div>
Додано:{" "}
{new Intl.DateTimeFormat("uk-UA").format(
new Date(product.createdAt),
)}
</div>
</li>
))}
</ul>
</section>
);
}У цьому прикладі:
ProductsPage виконується на сервері.
ProductList є Client Component.
products передається через межу клієнта.
ProductList використовує useState та onChange.
Фільтрація відбувається в браузері без нового запиту до сервера.
Props, що перетинають межу між Server і Client Components, мають бути серіалізованими.
Безпечними типами для такого передавання є:
рядки;
числа;
булеві значення;
null;
масиви;
прості об’єкти з такими значеннями.
Наприклад:
type UserInfo = {
id: number;
name: string;
roles: string[];
isActive: boolean;
};
<ClientProfile
user={{
id: 7,
name: "Олена",
roles: ["editor", "author"],
isActive: true,
}}
/>На практиці типи props краще оголошувати явно. Це допомагає виявити помилки ще під час перевірки TypeScript.
type Product = {
id: number;
name: string;
price: number;
};
type ProductListProps = {
products: Product[];
};Дані, отримані на сервері, часто містять значення, які не варто передавати напряму. Наприклад, дата може бути перетворена на ISO-рядок:
const product = {
id: 1,
name: "Ноутбук",
createdAt: new Date().toISOString(),
};У Client Component цей рядок можна перетворити на Date лише для відображення або обчислень:
const formattedDate = new Intl.DateTimeFormat("uk-UA").format(
new Date(product.createdAt),
);Також зазвичай потрібно перетворювати:
результати запитів до бази даних на прості об’єкти;
bigint на рядок або число, якщо це безпечно;
спеціальні об’єкти бібліотек на їхні прості представлення;
дати на рядки у визначеному форматі.
Корисний підхід — створювати окремий тип даних для клієнта:
type ProductFromDatabase = {
id: number;
name: string;
price: number;
createdAt: Date;
};
type ProductForClient = {
id: number;
name: string;
price: number;
createdAt: string;
};
function serializeProduct(
product: ProductFromDatabase,
): ProductForClient {
return {
id: product.id,
name: product.name,
price: product.price,
createdAt: product.createdAt.toISOString(),
};
}Так межа між серверною моделлю та даними для браузера стає явною.
Функція, оголошена в Server Component, не може бути звичайним prop для Client Component:
// Некоректний підхід
export default function Page() {
function handleSelect(id: number) {
console.log(id);
}
return <ProductList onSelect={handleSelect} />;
}Причина в тому, що функція існує в пам’яті сервера, а Client Component працює в іншому середовищі — у браузері. Функцію не можна просто серіалізувати як JSON і відновити на клієнті.
Обробник події потрібно оголосити всередині Client Component:
"use client";
import { useState } from "react";
type Product = {
id: number;
name: string;
};
export default function ProductList({
products,
}: {
products: Product[];
}) {
const [selectedId, setSelectedId] = useState<number | null>(null);
return (
<ul>
{products.map((product) => (
<li key={product.id}>
<span>{product.name}</span>
<button type="button" onClick={() => setSelectedId(product.id)}>
Обрати
</button>
</li>
))}
{selectedId !== null && <p>Обрано товар із ID: {selectedId}</p>}
</ul>
);
}Сервер передає дані, а клієнт визначає, що робити після взаємодії користувача.
childrenClient Component може отримати вже сформований сервером вміст через children. Це дає змогу додати інтерактивну оболонку, не перетворюючи весь вкладений вміст на клієнтський.
Файл components/TogglePanel.tsx:
"use client";
import { useState, type ReactNode } from "react";
type TogglePanelProps = {
title: string;
children: ReactNode;
};
export default function TogglePanel({
title,
children,
}: TogglePanelProps) {
const [isOpen, setIsOpen] = useState(false);
return (
<section>
<button type="button" onClick={() => setIsOpen((value) => !value)}>
{isOpen ? "Сховати" : "Показати"}: {title}
</button>
{isOpen && <div>{children}</div>}
</section>
);
}Файл app/dashboard/page.tsx:
import TogglePanel from "@/components/TogglePanel";
function ServerReport() {
return (
<div>
<h2>Звіт за сьогодні</h2>
<p>Цей вміст сформовано на сервері.</p>
</div>
);
}
export default function DashboardPage() {
return (
<main>
<h1>Панель керування</h1>
<TogglePanel title="Звіт">
<ServerReport />
</TogglePanel>
</main>
);
}TogglePanel відповідає за стан відкриття та закриття. ServerReport залишається Server Component і не стає клієнтським лише через те, що його передали як children.
Цей патерн особливо корисний для:
модальних вікон;
вкладок;
розгортаних панелей;
клієнтських контейнерів із серверним вмістом.
Звичайний потік даних має такий вигляд:
Server Component
│
│ серіалізовані props або children
▼
Client Component
│
│ локальний стан та події
▼
інтерфейс у браузеріServer Component може передати Client Component початкові дані. Client Component може змінювати власний стан, але це не змінює автоматично значення props у батьківському Server Component.
Наприклад:
"use client";
import { useState } from "react";
export default function Counter({ initialValue }: { initialValue: number }) {
const [value, setValue] = useState(initialValue);
return (
<div>
<p>Поточне значення: {value}</p>
<button type="button" onClick={() => setValue((current) => current + 1)}>
Збільшити
</button>
</div>
);
}initialValue надходить із сервера один раз для цього рендерингу. Подальші зміни value є локальним станом браузера.
Директива "use client" не означає, що весь застосунок стає клієнтським. Але всі імпорти Client Component утворюють його клієнтський граф залежностей.
Тому краще:
залишати сторінки Server Components;
передавати в клієнт лише дані, потрібні для інтерактивності;
не додавати "use client" до компонента без потреби;
розміщувати клієнтську межу якомога нижче в дереві компонентів.
Наприклад, якщо інтерактивною є лише кнопка, не обов’язково робити Client Component усю сторінку:
// app/page.tsx — Server Component
import LikeButton from "@/components/LikeButton";
export default function HomePage() {
return (
<main>
<h1>Стаття</h1>
<p>Цей текст може залишатися Server Component.</p>
<LikeButton initialLikes={10} />
</main>
);
}// components/LikeButton.tsx — Client Component
"use client";
import { useState } from "react";
export default function LikeButton({
initialLikes,
}: {
initialLikes: number;
}) {
const [likes, setLikes] = useState(initialLikes);
return (
<button type="button" onClick={() => setLikes((value) => value + 1)}>
Подобається: {likes}
</button>
);
}Так до браузера потрапляє лише необхідна інтерактивна частина.
Некоректно:
"use client";
import ServerProfile from "./ServerProfile";Client Component не повинен напряму імпортувати Server Component. Замість цього серверний компонент можна передати через children або інший підтримуваний слот.
Некоректно:
<ClientButton onClick={serverFunction} />Звичайна серверна функція не є серіалізованим значенням. Обробник клієнтської події потрібно оголосити в Client Component.
Не варто передавати через межу клієнта екземпляри класів, об’єкти моделей бази даних або значення зі специфічною поведінкою. Перетворіть їх на прості об’єкти:
const clientData = {
id: databaseRecord.id,
title: databaseRecord.title,
};"use client" до всієї сторінкиЦе може призвести до зайвого перенесення логіки та залежностей у браузер. Директива має бути лише там, де потрібні стан, події або браузерні API.
Props є вхідними даними компонента. Зміна локального стану в браузері не змінює автоматично джерело даних на сервері.
Server Components можуть передавати Client Components props.
Дані, що перетинають межу клієнта, мають бути серіалізованими.
Найнадійніше передавати рядки, числа, булеві значення, null, масиви та прості об’єкти.
Дати й інші спеціальні значення краще перетворювати на прості представлення, наприклад ISO-рядки.
Звичайні функції не можна передавати як props із сервера в клієнт.
Server Component можна передати до Client Component через children.
"use client" варто розміщувати якомога нижче в дереві компонентів.
Server Component зазвичай відповідає за отримання даних, а Client Component — за локальний стан та взаємодію користувача.