JavaRush /Курсы /Модуль 4: Node.js, Next.js и Angular /Взаимодействие Server ↔ Client Components: передача пропс...

Взаимодействие Server ↔ Client Components: передача пропсов

Модуль 4: Node.js, Next.js и Angular
7 уровень , 8 лекция
Открыта

1. Почему важно разделять Server и Client Components

В Next.js 15App Router в частности) вся архитектура построена вокруг разделения компонентов на две большие категории: серверные и клиентские. Это не просто прихоть разработчиков фреймворка — за этим стоит цель сделать приложения максимально быстрыми, безопасными и удобными для поддержки.

Server Components отлично подходят для работы с базами данных, файловой системой, секретами и прочими вещами, которые нельзя или не нужно отправлять в браузер. Они рендерятся на сервере, а результат — уже готовая разметка и данные — отправляется клиенту.

Client Components нужны для интерактивных элементов: кнопок, форм, выпадающих списков, управления состоянием и т.д. Они рендерятся и "оживают" уже в браузере пользователя.

Но что делать, если часть данных приходит с сервера, а часть логики — на клиенте? Как передать данные между этими мирами? Как не запутаться в потоках данных и не получить головную боль вместо красивого приложения? Вот тут и начинается магия передачи пропсов между Server и Client Components.

Как устроена передача пропсов между Server и Client Components

Основное правило

Server Component может рендерить Client Component и передавать ей пропсы.
Client Component не может рендерить Server Component.

Это похоже на эстафету: серверный компонент может "передать палочку" клиентскому, но не наоборот. Причина проста: сервер не знает о состоянии клиента, а клиент не может исполнить код, который должен выполняться только на сервере (например, обращение к базе данных).

Схема взаимодействия


[Server Component]
        |
        v
[Client Component]

Пояснение:

  • Server Component: получает данные (например, из базы), формирует пропсы.
  • Client Component: получает эти пропсы, отображает их, добавляет интерактивность.

2. Практический пример: список задач с кнопкой "Выполнить"

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

Server Component: получает данные и рендерит Client Component


// app/tasks/page.tsx (Server Component)
import { getTasks } from '@/lib/data'; // функция для получения задач с сервера
import TaskList from './TaskList';

export default async function TasksPage() {
  const tasks = await getTasks(); // Получаем задачи на сервере

  return (
    <section>
      <h1>Список задач</h1>
      <TaskList tasks={tasks} /> {/* Передаем задачи в клиентский компонент */}
    </section>
  );
}

Пояснение

  • getTasks — это функция, которая, например, читает задачи из базы данных или файла.
  • TaskList — это наш клиентский компонент (о нем чуть ниже).

Client Component: получает пропсы и добавляет интерактивность


// app/tasks/TaskList.tsx
'use client';

import { useState } from 'react';

export default function TaskList({ tasks }) {
  // Для примера: локальное состояние для отметки задач как выполненных
  const [completed, setCompleted] = useState({});

  function handleComplete(id) {
    setCompleted(prev => ({ ...prev, [id]: true }));
  }

  return (
    <ul>
      {tasks.map(task => (
        <li key={task.id}>
          <span style={{ textDecoration: completed[task.id] ? 'line-through' : 'none' }}>
            {task.title}
          </span>
          {!completed[task.id] && (
            <button onClick={() => handleComplete(task.id)}>Выполнить</button>
          )}
        </li>
      ))}
    </ul>
  );
}

Обратите внимание:

  • В начале файла стоит 'use client', чтобы явно указать Next.js, что компонент клиентский.
  • Проп tasks приходит от серверного компонента.
  • Внутри компонента мы можем использовать React-хуки, обработчики событий и всё, что доступно только на клиенте.

Как это работает?

  • Серверный компонент получает данные (например, из базы данных, API или файла).
  • Он рендерит клиентский компонент, передавая ему данные через обычные пропсы.
  • Клиентский компонент получает эти данные как входные и может их отображать, а также добавлять интерактивность (например, кнопки, обработчики кликов).

3. Что можно и что нельзя передавать как пропсы

Можно передавать:

  • Примитивы (строки, числа, boolean)
  • Простые объекты и массивы (которые можно сериализовать)
  • Функции, если они определены внутри Client Component (см. ниже)

Нельзя передавать:

  • Функции, определённые в Server Component (они не могут "поехать" в браузер)
  • Не сериализуемые объекты (например, классы, Date, Map, Set, если они не сериализованы)
  • React-элементы (например, нельзя передать <div /> как пропс)

Если вы попробуете передать что-то "несъедобное" (например, функцию или нестандартный объект), Next.js покажет ошибку сериализации.

Таблица: что можно передавать

Тип пропса Можно? Пример
string
"Привет, мир!"
number
42
boolean
true
массив
[1, 2, 3]
plain object
{ name: "Вася" }
функция (из Server)
() => ...
класс
new Date(), new Map()
React элемент
<div />

4. Передача пропсов: типизация и best practices

Если вы используете TypeScript (а в Next.js это рекомендуется), обязательно типизируйте пропсы!


// app/tasks/TaskList.tsx
'use client';

type Task = {
  id: number;
  title: string;
};

type Props = {
  tasks: Task[];
};

export default function TaskList({ tasks }: Props) {
  // ... остальной код
}

Best practice:

  • Старайтесь передавать только те данные, которые действительно нужны клиентскому компоненту.
  • Не передавайте большие объекты или "сырые" данные, если можно подготовить их на сервере.
  • Если нужно передать что-то сложное (например, Date), сериализуйте это в строку и обратно.

5. Передача пропсов вглубь: "проброс" через несколько уровней

Server Component может рендерить Client Component напрямую, а может — через промежуточные компоненты. Главное: вся цепочка до первого Client Component — серверная.

Пример:


// app/page.tsx (Server Component)
import Parent from './Parent';

export default async function Page() {
  const data = await fetchData();
  return <Parent data={data} />;
}

// app/Parent.tsx (Server Component)
import Child from './Child';

export default function Parent({ data }) {
  // Можно обработать/отфильтровать данные
  return <Child info={data} />;
}

// app/Child.tsx (Client Component)
'use client';

export default function Child({ info }) {
  // info — это данные, пришедшие с сервера через несколько уровней
  return <div>{JSON.stringify(info)}</div>;
}

Важно:

  • Как только цепочка доходит до Client Component, всё, что ниже по дереву, должно быть клиентским (или вложенные клиентские компоненты).
  • Client Component не может рендерить Server Component. Если попробовать — получите ошибку.

6. Ограничения и нюансы передачи пропсов

Почему нельзя рендерить Server Component внутри Client Component?
Потому что серверный компонент требует выполнения на сервере, а клиентский — уже работает в браузере. В браузере нет доступа к серверу, чтобы "досчитать" компонент на лету. Это похоже на попытку приготовить борщ в микроволновке на даче, когда все ингредиенты остались в супермаркете.

Как сделать так, чтобы Client Component получал "свежие" данные?

  • Обновлять данные через пропсы (например, при навигации или перерисовке страницы).
  • Использовать клиентские API (fetch, SWR, React Query) внутри Client Component, если нужно обновлять данные без перезагрузки страницы.

Передача функций как пропсов

  • Server Component не может передать функцию в Client Component.
  • Если нужна функция (например, обработчик клика), она должна быть определена внутри Client Component.
  • Можно передавать идентификаторы, а не функции. Например, передать id задачи, а обработчик реализовать на клиенте.

7. Применение в учебном приложении

Давайте добавим в ваше приложение ещё одну фичу: фильтрацию задач по статусу (выполнено/не выполнено).

Server Component: формирует данные


// app/tasks/page.tsx
import { getTasks } from '@/lib/data';
import TaskList from './TaskList';

export default async function TasksPage() {
  const tasks = await getTasks();
  return (
    <section>
      <h1>Список задач</h1>
      <TaskList tasks={tasks} />
    </section>
  );
}

Client Component: фильтрация и интерактивность


// app/tasks/TaskList.tsx
'use client';

import { useState } from 'react';

export default function TaskList({ tasks }) {
  const [showCompleted, setShowCompleted] = useState(true);
  const [completed, setCompleted] = useState({});

  function handleComplete(id) {
    setCompleted(prev => ({ ...prev, [id]: true }));
  }

  const filteredTasks = tasks.filter(
    task => showCompleted || !completed[task.id]
  );

  return (
    <div>
      <label>
        <input
          type="checkbox"
          checked={showCompleted}
          onChange={e => setShowCompleted(e.target.checked)}
        />
        Показывать выполненные
      </label>
      <ul>
        {filteredTasks.map(task => (
          <li key={task.id}>
            <span style={{ textDecoration: completed[task.id] ? 'line-through' : 'none' }}>
              {task.title}
            </span>
            {!completed[task.id] && (
              <button onClick={() => handleComplete(task.id)}>Выполнить</button>
            )}
          </li>
        ))}
      </ul>
    </div>
  );
}

Пояснение

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

8. Типичные ошибки при передаче пропсов между Server и Client Components

Ошибка №1: попытка передать функцию из Server Component в Client Component.
Это невозможно, потому что функция не может быть сериализована и передана в браузер. Всегда определяйте обработчики событий внутри Client Component.

Ошибка №2: попытка рендерить Server Component внутри Client Component.
Такой код не сработает и вызовет ошибку компиляции. Архитектура Next.js строго разделяет эти миры.

Ошибка №3: передача не сериализуемых объектов (например, Date, Map, Set) как пропсов.
Передавайте только простые объекты и массивы, которые можно преобразовать в JSON. Если нужно передать дату — сериализуйте её в строку, а на клиенте преобразуйте обратно.

Ошибка №4: забыли добавить 'use client' в компонент, который должен быть клиентским.
Без этой директивы компонент будет считаться серверным, и вы не сможете использовать хуки или обработчики событий.

Ошибка №5: чрезмерная вложенность Client Components, где можно обойтись Server Component.
Не делайте все компоненты клиентскими "на всякий случай". Оставляйте максимум логики и рендеринга на сервере, а клиентскими делайте только те компоненты, которым действительно нужна интерактивность.

Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ