5 мин. чтения

Next.js App Router: смешения серверных и клиентских компонентов — как исправить

Научитесь быстро находить и исправлять ошибки смешения серверных и клиентских компонентов в Next.js App Router: как реструктурировать компоненты, Server Actions и получение данных, чтобы избежать падений в рантайме.

Next.js App Router: смешения серверных и клиентских компонентов — как исправить

Что такое смешение серверного и клиентского кода?

Смешение серверного и клиентского кода происходит, когда часть кода выполняется не там, где должна.

В Next.js App Router некоторые компоненты предназначены для выполнения на сервере (безопасно для секретов, прямых вызовов БД, приватных API). Другие — для выполнения в браузере (клики по кнопкам, локальное состояние, доступ к window). Когда эти обязанности перепутаны, вы получите падения, проблемы с гидратацией или сборки, которые падают только после деплоя.

Полезная модель для мышления:

  • Серверные компоненты получают и подготавливают данные, затем передают простые props вниз.
  • Клиентские компоненты обрабатывают взаимодействие, но не должны тянуть серверный код.

В деве это часто выглядит нормально — режим разработки может быть более снисходительным. Hot reload, другая упаковка и тайминги могут скрывать проблемы границ. Продакшен‑сборки строже на то, что можно запустить в браузере, а гидратация менее терпима к несоответствиям.

Типичные симптомы:

  • Пустая страница после навигации (ошибка видна только в консоли)
  • Ошибки гидратации: UI мигнёт, затем ломается
  • Неожиданные 500‑ошибки при рендере
  • “window is not defined” или “document is not defined”
  • Ошибки сборки про импорт серверных модулей в клиентские файлы

AI‑сгенерированный код делает такие ошибки чаще, потому что фрагменты копируются без учёта границ. Типичный пример: добавить "use client" на всю страницу, чтобы заглушить ошибку с хуком, хотя страница импортирует хелпер для БД или читает секреты.

Как App Router разделяет серверные и клиентские компоненты

В App Router по умолчанию каждый компонент — Server Component. Это простое правило объясняет большинство сюрпризов.

Серверные компоненты (по умолчанию)

Серверные компоненты выполняются на сервере. Используйте их для получения данных, чтения cookies/headers, работы с environment‑секретами и любой тяжёлой логики, которую не хочется класть в браузер.

Если это касается базы данных, приватных API‑ключей или сессии аутентификации — держите это на сервере и передавайте результаты как простые props.

Клиентские компоненты (по желанию)

Компонент становится клиентским только когда вы добавляете "use client" в начале файла. Клиентские компоненты выполняются в браузере, поэтому они могут использовать состояние, эффекты, обработчики событий и браузерные API вроде localStorage.

Граница работает так:

  • Серверный компонент может импортировать клиентский компонент. Всё внутри этого клиентского компонента будет выполняться на клиенте.
  • Клиентский компонент не может импортировать серверный компонент или сервер‑только модули.

Обычно "use client" нужен, если компонент использует хуки вроде useState/useEffect, события браузера onClick или API типа window и document.

Вам не нужен "use client" для простой разметки, которая только рендерит props. Распространённый фикс — держать страницу и загрузку данных на сервере, а рендерить маленький клиентский компонент только для интерактивной части (фильтр, модалка или inline‑редактор).

Шаблоны падений рантайма, которые легко распознать

Большинство падений в App Router — одна и та же проблема: код для браузера выполняется на сервере, либо серверный код попадает в бандл для браузера.

Паттерн 1: хуки в серверном компоненте

Если вы видите ошибки вроде “React Hook ... is not supported in Server Components” или “You're importing a component that needs useState/useEffect”, проверьте шапку файла. Если файл не начинается с "use client", React будет считать его серверным компонентом.

Паттерн 2: серверные модули попали в клиентский код

Ошибки про fs, path, crypto или “Module not found: Can't resolve 'fs'” часто означают, что клиентский компонент импортировал общий хелпер, который (возможно косвенно) импортирует Node‑только код.

Такое часто случается, когда общий utils или lib смешивает серверные и клиентские помощники, а клиент импортирует его «только ради одной функции».

Паттерн 3: браузерные API используются во время серверного рендера

“window is not defined”, “document is not defined” и “localStorage is not defined” означают, что код выполняется на сервере. Это может быть серверный компонент, серверное действие или модуль, который импортируется в ходе серверного рендера.

Паттерн 4: вызов серверной логики с клиента без безопасного моста

Такие ошибки выглядят как “You're importing a Server Action into a Client Component”, “Server-only module cannot be imported from a Client Component” или просто клиентский вызов функции, которая никогда не должна была выполняться в браузере.

Пошагово: найдите плохую границу в дереве компонентов

Самые быстрые победы приходят от воспроизводимой ошибки. Попробуйте воспроизвести её в деве и в прод‑сборке. “Работает в dev” — не значит, что границы в порядке.

Когда вы смотрите на ошибку, остановитесь на первом файле, который действительно принадлежит вам. Фреймворковые трассировки громоздки. Первый файл в вашем репозитории обычно — место, где в дерево попадает неверный импорт или тип компонента.

Простой рабочий процесс:

  • Воспроизведите падение одинаково каждый раз (тот же путь, то же действие, то же состояние пользователя).
  • В трассировке стека перейдите к первому app‑файлу и посмотрите, какой компонент его отрендерил.
  • Проверьте шапку файла: серверный это компонент по умолчанию или он начинается с "use client"?
  • Идите по импортам, пока не найдёте первое несоответствие:
    • серверный импорт используется в клиентском коде (fs, клиенты БД, next/headers)
    • браузерное использование внутри серверного кода (window, document, localStorage)
  • Примите решение по владению: секреты и данные — на сервере, состояние UI и события — на клиенте.

Очень частая ошибка: серверная страница передаёт клиентскому компоненту клиенту объект подключения к БД, данные, полученные из cookies, или серверный хелпер. Это сломает всё. Делайте fetch на сервере, затем передавайте простые JSON‑данные вниз.

Реструктурирование компонентов, смешивающих сервер и клиент

Падения обычно происходят, когда один компонент пытается делать всё сразу.

Надёжное разделение:

  • Серверный компонент: получает данные, проверяет аутентификацию, использует секреты.
  • Клиентский компонент: управляет состоянием, событиями, эффектами и любым UI‑кодом, завязанным на DOM.

Поднимите работу с данными выше по дереву. Доставайте данные в серверном компоненте (или в серверной функции, вызываемой им), затем передавайте результат как простые props. Это не даст серверному коду попасть в клиентский бандл.

Изолируйте интерактивность. Держите клиентские части маленькими, чтобы не отправлять всю страницу в браузер ради одной кнопки.

На границе сервер→клиент держите props простыми: строки, числа, булевы, массивы, простые объекты. Не передавайте клиентов БД, объекты запроса, экземпляры классов или функции.

Пример: dashboard‑страница получает данные пользователя, подписку и недавнюю активность, но имеет фильтры, модалку и график. Получите всё в DashboardPage (сервер) и передайте { userName, plan, activityItems } вниз. DashboardControls — клиентский компонент — отвечает за состояние фильтров и открытие/закрытие модалки.

Server Actions: безопасные паттерны для форм и мутаций

Получите практический план
Получите практический план по стабилизации codebase перед добавлением новых фич.

Server Actions хорошо подходят, когда пользователь отправляет форму и нужно изменить данные: создать запись, обновить профиль, сбросить пароль или выполнить небольшой workflow.

Безопасная структура: держите UI формы в клиентском компоненте, а мутацию — в серверном файле как экспортируемое действие. Клиент управляет полями, состоянием загрузки и отображением ошибок. Сервер выполняет проверку авторизации, валидацию и обращения к БД.

// actions.ts
'use server'

export async function updateProfile(formData: FormData) {
  const name = String(formData.get('name') ?? '')
  // validate, check auth, write to DB
  return { ok: true }
}

На стороне клиента передавайте только то, что действительно нужно серверу. Не гоните секреты, токены или сырые объекты пользователя через props только чтобы action сработал. Если действию нужно знать, кто пользователь — прочитайте это на сервере (cookies/session) внутри action.

Две привычки предотвращают большинство утечек:

  • Валидируйте ввод и перепроверяйте авторизацию внутри action.
  • Возвращайте понятные, безопасные ошибки для пользователей, а не трассировки стека.

Если нужен оптимистичный UI, держите его локальным и небольшим. Не делайте всю страницу клиентской только ради индикатора загрузки.

Получение данных в App Router без лишней работы

Многие проблемные границы начинаются с двойного получения данных.

В App Router по умолчанию желательно делать fetch на сервере близко к маршруту. Так вы получите более быстрый первый рендер, меньше бандлы в браузере и сохраните секреты в серверной среде.

Делайте запросы на клиенте только когда это действительно нужно (polling, реальные виджеты, или кнопка обновления для одной секции).

Распространённая ошибка: серверный рендер получает данные, затем клиентский компонент монтируется и снова делает fetch в useEffect. Это вызывает мерцание, проблемы с лимитами и запутанные несоответствия.

Чистый поток выглядит так:

  • Запрос приходит на маршрут
  • Серверный fetch получает данные (БД, внутренний API или сторонний сервис)
  • Серверные компоненты рендерят страницу с этими данными
  • Клиентские компоненты обрабатывают взаимодействия и запускают целевые обновления

Кэширование тоже может скрывать проблемы при тестировании. Если данные выглядят случайно устаревшими, проверьте, не кэшируется ли fetch и как настроена рева‑валидация.

Аутентификация и секреты: что должно оставаться на сервере

Найдите реальную багу границы
Мы точно укажем первый неверный сервер‑клиент границу и импорты, вызывающие сбой.

Проблемы с аутентификацией часто начинаются с ошибки границы: клиентский компонент трогает то, что никогда не должно покидать сервер. Иногда это вызывает падения или странные редиректы. Иногда вы тихо сливаете секреты в клиентский бандл.

Чаще всего в сгенерированном коде утечки:

  • Чтение переменных окружения в клиентском компоненте
  • Помещение конфигурации в общий файл, который импортируется и сервером, и клиентом

Если это будет тяжело увидеть в DevTools — этого не должно быть доступно клиенту.

Держите проверки авторизации и логику ролей на сервере. Клиент может рендерить состояния UI, но не должен быть источником истины для «разрешён ли пользователь».

Избегайте хранения чувствительных токенов в localStorage по умолчанию. Это легко посмотреть и может быть украдено через XSS.

Быстрые поломки, за которыми стоит понаблюдать:

  • Циклы редиректов, когда и сервер, и клиент пытаются защищать один и тот же маршрут
  • Несоответствия сессий, когда сервер отрендерил одно состояние, а клиент гидрируется в другое
  • Путаница runtimes (Edge vs Node) в библиотеках для auth
  • “Работает локально, ломается в проде”, когда env vars отличаются и клиентские бандлы меняются

Частые ошибки, из‑за которых падения возвращаются снова и снова

Большинство повторяющихся падений — не таинственные баги фреймворка. Это те же ошибки границ, залеченные наспех и затем снова введённые.

Пара закономерных паттернов:

  • Добавление "use client" на большую страницу, чтобы заглушить ошибку с хуком
  • Общие хелперы, которые смешивают серверный и клиентский код
  • Создание или импорт клиента БД прямо в файлах компонентов (распространяется через импорты очень быстро)
  • Вызов fetch() к собственному API из серверного компонента по привычке, хотя можно вызвать серверный код напрямую
  • Фиксы методом проб и ошибок вместо следования к первому плохому импорту в стеке

Типичный пример: dashboard‑страница падает только в проде, потому что импортирует getUser() (читает cookies, сервер‑только), но страницу пометили "use client" ради графика. Надёжный фикс — вынести график в отдельный клиентский компонент и оставить страницу сервер‑первой.

Быстрая чек‑лист перед отправкой в прод

Большинство падений App Router — результат одного файла, выполняющего две задачи.

Проверка границ

Спросите для каждого компонента: может ли этот файл выполниться в браузере?

Если да — он не должен трогать секреты, сервер‑только env‑переменные, клиентов БД или библиотеки Node. Если вы видите такие импорты — вынесите работу в Серверный Компонент, Server Action или серверный маршрут.

Финальная проверка:

  • Browser API (window, document, localStorage, navigator) и хуки — значит Клиентский Компонент. Убирайте серверную логику.
  • Секреты и сервер‑только импорты — значит Серверный Компонент. Передавайте только нужные данные для UI.
  • Props, пересекающие границу, должны быть сериализуемыми (простые объекты, массивы, строки, числа). Избегайте экземпляров классов, BigInt и функций.
  • Для операций записи (формы, обновления, удаления) используйте Server Action или серверный маршрут.
  • Тестируйте продакшен‑сборку локально, а не только next dev.

Одна практическая привычка

Перед деплоем прогоните основные сценарии после чистой сборки. Если страница падает только в прод режиме, скорее всего это проблема границы, несериализуемый prop или сервер‑только импорт, попавший в клиент.

Пример: починка падающей dashboard‑страницы

Остановите падения Next.js только в проде
Отправьте репозиторий — мы исправим смешения App Router, которые ломаются только в проде.

Классическая ситуация: dashboard‑страница нужна серверно‑полученные данные пользователя плюс интерактивные фильтры (диапазон дат, переключатели статуса, поиск).

Где ошибка

Первая версия часто смешивает обязанности в одном файле. Например, app/dashboard/page.tsx получает данные на сервере, но также использует useState, читает localStorage или вызывает window.matchMedia для запоминания настроек фильтров. В браузере это выглядит ок, но по умолчанию страница — Server Component, поэтому может упасть с “window is not defined” или “Hooks can only be used in a Client Component.”

Ещё частая оплошность: filter UI помечен 'use client', но импортирует сервер‑только хелпер, который читает cookies или обращается к приватной базе. Это вызовет ошибки вида “You’re importing a Server Component into a Client Component”.

Простая реструктуризация, которая останавливает падения

Сделайте страницу ответственной за данные, а клиентский компонент — за интерактивность.

На сервере (page): получаете данные и рендерите их.

// app/dashboard/page.tsx (Server Component)
import Filters from './Filters';
import { getDashboardData } from './data';

export default async function Page() {
  const data = await getDashboardData();
  return (
    <>
      <Filters initial={data.filters} />
      {/* render table using data.items */}
    </>
  );
}

На клиенте (filters): держите состояние и UI‑события локальными и отправляйте изменения через Server Action.

// app/dashboard/actions.ts
'use server';
export async function updateFilters(next) {
  // validate input, save, return safe data
  return { ok: true };
}

Результат: меньше падений во время выполнения, чище ответственность (данные и секреты остаются на сервере, клиент обрабатывает клики), а обновления проходят по одному безопасному пути.

Что делать дальше, если App Router продолжает ломаться

Если та же ошибка повторяется снова и снова, скорее всего в проекте системная проблема границ: клиент тянет сервер‑только модули, сервер импортирует хуки, или мутации разбросаны по клиенту.

AI‑сгенерированные кодовые базы от инструментов вроде Lovable, Bolt, v0, Cursor или Replit склонны повторять эти ошибки, потому что смешивают паттерны из старых установок, которые не выдерживают строгих правил App Router.

Когда одинаковые симптомы проявляются на нескольких страницах, фокусированный рефактор часто быстрее, чем латание.

Если вы унаследовали сломанный AI‑прототип и хотите быстрый структурированный диагноз, FixMyMess (fixmymess.ai) специализируется на ремонте и укреплении таких Next.js кодовых баз, начиная с бесплатного аудита кода, чтобы найти первые ошибки границ и рискованные импорты.

Next.js App Router: смешения серверных и клиентских компонентов — как исправить | fixmymess.ai