5. Server vs Client Components
⚡ Server Components vs Client Components
Заголовок раздела «⚡ Server Components vs Client Components»Это самая важная концепция в современном Next.js. React Server Components (RSC) меняют способ мышления о рендеринге. Давай разберём раз и навсегда! 🧠
Старый React (всё на клиенте):Browser → загружает JS → React рендерит HTML → показывает пользователю
Next.js RSC (разделение):Server → рендерит Server Components → отправляет HTML + Client ComponentsBrowser → получает готовый HTML → "оживляет" Client Components (hydration)🖥️ Server Components: По умолчанию
Заголовок раздела «🖥️ Server Components: По умолчанию»В App Router все компоненты серверные по умолчанию. Без единой директивы:
// app/products/page.tsx — Server Component!// Никакого 'use client', никаких пометок
import { db } from '@/lib/db'; // 🔒 Безопасно — только на сервереimport { cache } from 'react';
// async/await прямо в компоненте!export default async function ProductsPage() { // Прямой запрос к базе данных const products = await db.product.findMany({ include: { category: true }, orderBy: { createdAt: 'desc' }, take: 20, });
return ( <main> <h1>Товары ({products.length})</h1> <ul> {products.map(product => ( <li key={product.id}> {product.name} — {product.price}₽ </li> ))} </ul> </main> );}Что могут Server Components:
// 1. Асинхронные запросы прямо в компонентеasync function UserCard({ userId }: { userId: string }) { const user = await fetchUser(userId); // Нет useEffect! return <div>{user.name}</div>;}
// 2. Прямой доступ к серверным ресурсамimport { db } from '@/lib/prisma';import { cookies } from 'next/headers';import { headers } from 'next/headers';
async function SecureData() { const cookieStore = await cookies(); const token = cookieStore.get('auth-token');
const headersList = await headers(); const userAgent = headersList.get('user-agent');
// Прямо к БД! const data = await db.secret.findMany();
return <div>{data.length} записей</div>;}
// 3. Доступ к переменным окружения (серверным!)async function Config() { const apiKey = process.env.SECRET_API_KEY; // Не утечёт в браузер! const data = await fetch('https://api.service.com', { headers: { 'X-API-Key': apiKey! } }); return <div>Данные загружены</div>;}
// 4. Импорт heavy библиотек без влияния на bundleimport { marked } from 'marked'; // Не попадёт в JS-бандл!import { highlight } from 'shiki'; // Только на сервере
async function MarkdownRenderer({ content }: { content: string }) { const html = await marked(content); return <div dangerouslySetInnerHTML={{ __html: html }} />;}Что НЕ могут Server Components:
// ❌ useState — нет// ❌ useEffect — нет// ❌ useRef — нет// ❌ Event handlers (onClick, onChange...) — нет// ❌ Browser APIs (window, document, localStorage) — нет// ❌ Context (useContext) — нет (можно передавать через props)// ❌ Class components с lifecycle методами — нет
// Всё это только в Client Components!💻 Client Components: Интерактивность
Заголовок раздела «💻 Client Components: Интерактивность»Client Components нужны для интерактивности. Пометь их директивой 'use client':
'use client'; // Эта директива делает компонент клиентским
import { useState, useEffect } from 'react';
export function Counter({ initialCount = 0 }: { initialCount?: number }) { const [count, setCount] = useState(initialCount);
useEffect(() => { document.title = `Счётчик: ${count}`; }, [count]);
return ( <div> <p>Счётчик: {count}</p> <button onClick={() => setCount(c => c + 1)}>+1</button> <button onClick={() => setCount(c => c - 1)}>-1</button> </div> );}Что могут Client Components:
'use client';
// ✅ Все хуки Reactimport { useState, useEffect, useRef, useCallback, useMemo } from 'react';import { useRouter, usePathname, useSearchParams } from 'next/navigation';
// ✅ Event handlersfunction Form() { const [value, setValue] = useState('');
return ( <form onSubmit={e => { e.preventDefault(); console.log(value); }}> <input onChange={e => setValue(e.target.value)} value={value} /> <button type="submit">Отправить</button> </form> );}
// ✅ Browser APIsfunction LocalStorageExample() { const [theme, setTheme] = useState<string>(() => { // Работает только на клиенте! return typeof window !== 'undefined' ? localStorage.getItem('theme') ?? 'light' : 'light'; });
const toggleTheme = () => { const next = theme === 'light' ? 'dark' : 'light'; setTheme(next); localStorage.setItem('theme', next); };
return <button onClick={toggleTheme}>Тема: {theme}</button>;}
// ✅ Third-party клиентские библиотекиimport { motion } from 'framer-motion';import { Swiper, SwiperSlide } from 'swiper/react';
function AnimatedCard() { return ( <motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }} whileHover={{ scale: 1.05 }} > Анимированная карточка </motion.div> );}🌊 Граница между Server и Client
Заголовок раздела «🌊 Граница между Server и Client»Ключевое правило: 'use client' создаёт границу. Всё ниже этой границы — клиентский код:
// app/page.tsx — Server Componentimport { ClientSearch } from './ClientSearch'; // Импорт клиентского компонента
export default async function Page() { const posts = await fetchPosts(); // Серверный код
return ( <div> <h1>Блог</h1> <ClientSearch /> {/* Граница! Внутри — клиентский код */} <PostList posts={posts} /> {/* Серверный компонент */} </div> );}'use client'; // Граница начинается здесь
import { useState } from 'react';// ВСЁ что импортируется здесь — тоже становится клиентским!
export function ClientSearch() { const [query, setQuery] = useState(''); // ... логика поиска}📦 Паттерны: как сочетать Server и Client
Заголовок раздела «📦 Паттерны: как сочетать Server и Client»Паттерн 1: Server Component как контейнер данных
// app/dashboard/page.tsx — Server Componentimport { UserProfile } from './UserProfile'; // Client Component
export default async function DashboardPage() { // Загружаем данные на сервере const user = await db.user.findUnique({ where: { id: session.userId } }); const stats = await getStats(session.userId);
return ( <div> {/* Передаём серверные данные в клиентский компонент */} <UserProfile user={user} // Сериализуемые данные! stats={stats} /> </div> );}
// components/UserProfile.tsx — Client Component'use client';
interface Props { user: { id: string; name: string; email: string }; stats: { posts: number; views: number };}
export function UserProfile({ user, stats }: Props) { const [editing, setEditing] = useState(false);
return ( <div> <h2>{user.name}</h2> {/* Интерактивность на клиенте */} <button onClick={() => setEditing(true)}>Редактировать</button> </div> );}Паттерн 2: Интерлив Server и Client
// ✅ Server Component может рендерить Client Component// ✅ Client Component может рендерить Server Component через props children
// app/page.tsx — Server Componentimport { InteractiveWrapper } from '@/components/InteractiveWrapper'; // Client
export default async function Page() { const data = await fetchData();
return ( <InteractiveWrapper> {/* Это — Server Component, переданный как children */} <ServerDataDisplay data={data} /> </InteractiveWrapper> );}
// components/InteractiveWrapper.tsx — Client Component'use client';import { useState } from 'react';
export function InteractiveWrapper({ children }: { children: React.ReactNode }) { const [expanded, setExpanded] = useState(false);
return ( <div> <button onClick={() => setExpanded(!expanded)}> {expanded ? 'Свернуть' : 'Развернуть'} </button> {expanded && children} {/* children — Server Component! */} </div> );}// children выполнены на сервере, WrapperClient — на клиентеПаттерн 3: Context с RSC
// Context работает только в Client Components!
'use client'; // Обязательно!
import { createContext, useContext, useState } from 'react';
const ThemeContext = createContext<{ theme: string; toggle: () => void;} | null>(null);
export function ThemeProvider({ children }: { children: React.ReactNode }) { const [theme, setTheme] = useState('light');
return ( <ThemeContext.Provider value={{ theme, toggle: () => setTheme(t => t === 'light' ? 'dark' : 'light') }}> {children} </ThemeContext.Provider> );}
export function useTheme() { const ctx = useContext(ThemeContext); if (!ctx) throw new Error('useTheme должен быть внутри ThemeProvider'); return ctx;}
// app/layout.tsx — Server Component (!)import { ThemeProvider } from '@/providers/ThemeProvider';
export default function RootLayout({ children }) { return ( <html> <body> {/* ThemeProvider — Client Component, но children могут быть серверными */} <ThemeProvider> {children} </ThemeProvider> </body> </html> );}🏗️ Гидрация (Hydration)
Заголовок раздела «🏗️ Гидрация (Hydration)»Гидрация — процесс “оживления” серверного HTML на клиенте:
Сервер:1. Рендерит HTML с данными2. Сериализует состояние компонентов (React Server Component Payload)3. Отправляет HTML + JS клиенту
Клиент:4. Браузер показывает HTML (контент виден сразу!)5. Загружается JavaScript (только клиентские компоненты!)6. React "gидрирует" DOM — прикрепляет обработчики событий7. Приложение становится интерактивным// Ошибки гидрации — частая проблема!// Возникает когда серверный и клиентский HTML различаются
// ❌ Плохо — Math.random() разный на сервере и клиентеfunction BadComponent() { return <div id={Math.random().toString()}>Контент</div>;}
// ❌ Плохо — Date.now() разныйfunction BadDate() { return <div>{new Date().toISOString()}</div>;}
// ✅ Хорошо — используй useEffect для клиентского контента'use client';function GoodComponent() { const [id, setId] = useState<string | null>(null);
useEffect(() => { setId(Math.random().toString()); // Только на клиенте }, []);
if (!id) return null; return <div id={id}>Контент</div>;}
// ✅ Или suppressHydrationWarning для элементов с динамическим контентомfunction TimeDisplay() { return ( <time suppressHydrationWarning> {new Date().toISOString()} </time> );}📚 Сторонние библиотеки с RSC
Заголовок раздела «📚 Сторонние библиотеки с RSC»Многие npm-пакеты написаны до RSC и используют клиентские APIs:
// ❌ Ошибка: библиотека использует useState внутриimport { SomeUILibComponent } from 'some-ui-lib';// → Error: useState can only be used in Client Components
// ✅ Решение 1: Обернуть в Client Component// components/ClientSomeUI.tsx'use client';import { SomeUILibComponent } from 'some-ui-lib';export { SomeUILibComponent }; // Re-export как клиентский
// ✅ Решение 2: Создать wrapper// components/ClientWrapper.tsx'use client';import { SomeUILibComponent } from 'some-ui-lib';
export function MyComponent(props: SomeProps) { return <SomeUILibComponent {...props} />;}
// Теперь можно использовать в Server Component:import { MyComponent } from '@/components/ClientWrapper';🎯 Резюме урока
Заголовок раздела «🎯 Резюме урока»| Server Components | Client Components | |
|---|---|---|
| Директива | Нет (по умолчанию) | 'use client' |
| useState/useEffect | ❌ | ✅ |
| async/await | ✅ | ❌ (useEffect) |
| DB / API secrets | ✅ | ❌ |
| onClick, onChange | ❌ | ✅ |
| Browser APIs | ❌ | ✅ |
| В bundle клиента | ❌ (не попадает!) | ✅ |
Правило большого пальца: Начни с Server Component. Добавь 'use client' только когда действительно нужно.