Правільна нарабатка межы ‘use client’ у Next.js App Router
Дазвольце дазнацца, што на самай працо паказвае дырэктыва ‘use client’, чаму маркаванне контейнераў збільшвае размер пакетаў, і як компаненты-листы і слоты-дзеці прымушваюць серверную работу выконвацца на самам серверы.
Адзьнаваючыся з дарабочымі кантэкстамі App Router, вы пабачыце, што ў верхней часты бяроз компанентаў знаходзіцца 'use client'. Штатна прычынка гэтага быстра сформуецца: калі вы дадзеце onClick або useState для выпадаючага меню, процес зборкі паведаміць пра проблему, а дырэктыва пазбяжыць яе. Павтараючы гэта колькісць дзесяткаў разоў, весь структураны кантэкст апынуцца ў пакете браузера, які стане аплікацыяй з адной сторанай з дадатковымі элементамі. У гэтым кяле пояснюецца, што на самай працэ дырэктыва і як яе размесціў, каб робота на серверы засталася там, а толькі справжнія інтэрактывныя часткі аплікацыі былі выданы як JavaScript.
Што на самай працэ пазначае дырэктыва
Назва можа супакоўваць, што гэта «выконваць толькі ў браузеры», але гэта не яго значэнне. 'use client' адзначае межу модуля: файл, дзе заканчываецца граф сэрверных модуляў і пачынаецца пакет кліента. Компаненты кліента все равно адразу перетвараюцца на HTML на сэрверы; пасля чаго яны дадаткова ініціязуюцца ў браузеры.
[ Server Component Tree ] (Executes solely on the server; zero KB client JS)
│
├── Server Component A (Fetches directly from DB)
│
▼
── [ 'use client' Boundary ] ──
│
├── Client Component B (Pre-rendered to HTML on server, hydrated on client)
│ │
│ └── Regular Component C (Now forced into the client bundle!)
Наследкі, які створюют працэзус для команд, є транзітывнымі. Кожны модуль, імпортаваны файлам з атрыбутам 'use client', таксама стае часткай кліентскага пакета, незалежна ад таго, чыме ён сам мае такій дырэктыва чы не. Якщо раскладка галоўнага панелі керування пазначыта як кліентскі модуль таму, што ў яе є спадны список з профілямі, то яе бокавая панель, модалкі, дапаможнікі і ўсе важкі бібліятэкі, якія ён імпортуе, таксама падаюць у браузер. Чыба прыгледзецца, чаму код, які выконваецца толькі на серверы, не займае жадных байтов у пакете, можна прачытаць у стацыі пра тое, як React Server Components даходзяць да нульовага рэндарынгу без пакета.
Пастка інтэрактыўнага контейнера
Найчастэйшая памылка — ператвараць цэлую сторанку ў кліентскі компонент таму, што яго маленька частка ёсць інтэрактыўная. У прыкладзе нижчэй для фільтрацыі патрэбны толькі данні стану, але весь панель керавання пазначаецца як кліентскі код:
// ❌ BAD: The entire page is pulled into the client bundle
'use client';
import { useState, useEffect } from 'react';
import { db } from '@/lib/db'; // 🚨 Build error or massive security/bundle hazard!
export default function UserDashboard() {
const [isOpen, setIsOpen] = useState(false);
const [data, setData] = useState(null);
useEffect(() => {
// You've recreated client-side waterfall fetching
fetch('/api/user-data').then(res => res.json()).then(setData);
}, []);
return (
<div className="p-8">
<button onClick={() => setIsOpen(!isOpen)}>Toggle Filter</button>
{isOpen && <div className="dropdown">...</div>}
<div className="grid">
{/* Render heavy data tables */}
</div>
</div>
);
}
Вылучаюцься два проблемы. Імпорт кліента базы дадзеных у кліентскі модуль або спрычыняе паўстанне, або, што горш, стварае рызык перанесення коду та настройкаў, прызначанных толькі для сервера, у браузер. А завантажэнне дадзеных пераводзіцца ў useEffect, таму сторанка атрымлівае порожній контэнт, а пасля гідратацыі запрашае дадзеныя, занова ствараючы той самы ланцоўкі аперацый на кліентскай стороне, якія мялі бы быць усунуты серверскімі компонентамі. Дадаўшы пакет server-only у такія модулі, як @/lib/db, першую памылку ператвараюць у явную паўстанневую памылку.
Перанесіце інтэрактыўнасць да самых ніжных элементаў
Даннэ прыналежыць самай маленькай складовай, якая яго патрэбуе. Чыраг становіцца самастайным кліентскім модулем, а все, што ён паказвае, перадаецца як children:
// components/FilterToggle.tsx
'use client';
import { useState } from 'react';
export function FilterToggle({ children }: { children: React.ReactNode }) {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<button
onClick={() => setIsOpen(!isOpen)}
className="px-3 py-1.5 bg-neutral-100 rounded text-sm"
>
{isOpen ? 'Hide Filters' : 'Show Filters'}
</button>
{isOpen && <div className="mt-2">{children}</div>}
</div>
);
}
Старонка застаецца асінхронным серверскім компанентам. Яна безпасябедна запытвае базу дадзеных, атрыбутуе статычны маркап і вбудоввае чыраг толькі там, дзе адварцяваецца:
// app/dashboard/page.tsx (Server Component by default)
import { db } from '@/lib/db';
import { FilterToggle } from '@/components/FilterToggle';
import { AnalyticsChart } from '@/components/AnalyticsChart';
export default async function UserDashboard() {
// Direct DB access — zero client-side fetch waterfalls
const metrics = await db.metrics.findMany();
return (
<main className="p-8">
<div className="flex justify-between items-center mb-6">
<h1 className="text-xl font-bold">Performance Analytics</h1>
<FilterToggle>
<p className="text-sm text-neutral-500">Filter options...</p>
</FilterToggle>
</div>
<div className="grid grid-cols-3 gap-4">
{/* AnalyticsChart can also remain an RSC if it doesn't need canvas/DOM APIs */}
<AnalyticsChart data={metrics} />
</div>
</main>
);
}
Параграф, пераданы FilterToggle, атрыбутуецца на серверзе, нават які ён з’яўляецца внутрь кліентскага компанента. AnalyticsChart можа застацца серверскім компанентам, як толькі ён не выкарыстоўвае API, якія дзейнаць толькі ў браузеры, такія як canvas; як толькі ён іх выкарыстоўвае, толькі гэты чарт патрабуе дырективы.
Складанне серверскага кантэнту внутрь кліентскіх шэлакоў
Тая ж самая тэхніка можа быть выкорыстана для структуравання лейаутаў. Праіснаваймо компонент кліента CollapsibleShell, які керуе станам адкрытага чы роўна закрытага стану, падобна до FilterToggle. Лейаут, які ўжо ёсць серверным компонентам, стварае даскладны фід і передае яго шэлу як элемент:
// app/layout.tsx
import { CollapsibleShell } from '@/components/CollapsibleShell';
import { ExpensiveServerFeed } from '@/components/ExpensiveServerFeed';
export default function RootLayout() {
return (
<html lang="en">
<body>
<CollapsibleShell>
{/* ExpensiveServerFeed runs on the server, streams HTML, and ships 0kb JS */}
<ExpensiveServerFeed />
</CollapsibleShell>
</body>
</html>
);
}
Паколькі ExpensiveServerFeed ствараецца ў серверным контэксте і перадаецца як children, ён адрасаваецца выключна на серверы. React прышле яго адрасаваны выхід у пакете RSC, і код компонента ніколі не прабягае ў бандл кліента. Ключовая разліка: кліентскі модуль, які імпортуе компонент, запрашвае яго ў бандл, тады калі кліентскі компонент прыме вялікі колькасць вучоў як пропсы, ён гэта не робіць.
Спіс пераконтрацоў пры дадаванні дырэктывы
- Чы рэгулярны компанент выкарыстоўвае стацыю, эфекты, працэсары змаганняў чы API браузера? Якщо нята, заставіце яго на серверы.
- Чы можна выдзельіць інтэрактыўную частку ў меншы дзеціны компанент?
- Чы можна перадаць кантэнт, выробленаў на серверы, через
childrenчы іншы проп у замену на імпорт? - Чы всі пропы, якія пераходзяць межу, ўможлівы да серыялізацыі, без функцый чы экзантэнсіяў класаў, за выключэнням Server Actions?
- Чы пазначэнне гэтага файла прыведзе да запуску важкай бібліятэкі, такой як парсер markdown чы інструменты для роботы з датамі, якія маглі бы застацца на серверы?
Ключовыя выводы
- Серверныя компаненты ўжо з прычыны являюцца стандартам; трэба заставіць там запошчанне дадзеных, інструменты для парсавання чы статычны маркап.
- Ізолюйце маленькія інтэрактыўныя элементы замест таго, каб ператвараць іх контейнеры.
children і іншыя атрыбуты слота, каб загаварыць выход сервера ў кліентскую логіку без яго адправкі.'use client' як даўальнай архітэктурной межы, а не як спосабу паспакоўваць канфлікты падчас зборкі. Якша выконана, пакеты застаюцца маленькімі, а змест выкарыстоўваецца ведаючы ўжо на стадіі першага адрасавання.