open atlas
↑ К треку
Паттерны React RXP · 14 · 01

Божественные компоненты и проброс пропсов

Божественные компоненты и глубокий проброс пропсов — это формы низкой связности и высокой связанности в React; лечи их композицией, кастомными хуками и колокацией состояния — и не переусердствуй с пробросом, ставя контекст или глобальный стор там, где хватило бы children.

RXP Senior ◷ 19 min
Уровень
ОсновыJuniorMiddleSenior

Открой почти любую стареющую кодовую базу на React — и найдёшь там одни и те же две формы. Первая — божественный компонент: один файл, который грузит данные, владеет десятком useState, прогоняет бизнес-правила и раскладывает экран — 500 строк, которые вся команда боится трогать, потому что каждая обязанность живёт в одном радиусе поражения. Вторая — проброс пропсов: значение, продетое сквозь шесть компонентов, которые им не пользуются, лишь бы дойти до того единственного внизу, кому оно нужно.

Ни то, ни другое не баг React. Это формы двух древних проектных запахов в React — низкой связности (один блок делает несвязанные вещи) и высокой связанности (изменение здесь вынуждает правки там). Хорошая новость: лекарства ты уже выучил в предыдущих юнитах. Этот урок — про то, как распознать запах и тянуться к самому лёгкому лекарству, а не к самому тяжёлому.

Цель

После этого урока ты можешь назвать божественный компонент низкой связностью, а глубокий проброс пропсов — связанностью, которую он создаёт; разложить божественный компонент на сфокусированные части через композицию, кастомные хуки и колокацию состояния; выбрать между тремя лекарствами от проброса — передачей JSX как children, подъёмом структуры и контекстом — по тому давлению, на которое каждое отвечает; и избежать senior-ловушки «починки» проброса контекстом на всё приложение или глобальным стором, когда правильным и более лёгким решением была композиция.

1

Божественный компонент — это низкая связность: он владеет несвязанными обязанностями, поэтому у него много причин меняться. Загрузка, локальное состояние UI, бизнес-логика и раскладка — это четыре разных работы. Собери их вместе — и каждая из них (новый эндпоинт, новый фильтр, смена правила, редизайн) правит один и тот же файл, и каждая правка рискует задеть остальные. Признак — компонент, который не описать одним предложением без «и».

// запах: один компонент, четыре работы, не описать без «и»
function OrdersPage() {
  const [orders, setOrders] = useState<Order[]>([]);
  const [status, setStatus] = useState("all");
  const [sort, setSort] = useState("date");
  const [page, setPage] = useState(1);
  const [selected, setSelected] = useState<string[]>([]);

  useEffect(() => { /* fetch + parse + retry */ }, [status, page]);

  const visible = orders /* фильтр по статусу, потом сортировка, потом срез страницы */;
  const revenue = visible.reduce((s, o) => s + o.total, 0); // бизнес-правило

  return <div>{/* тулбар + таблица + пагинация + сводка, всё инлайн */}</div>;
}

Связность — это вопрос «всё ли в этом блоке принадлежит друг другу?». Здесь ответ явно нет.

2

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

// один хук владеет «какие заказы показать»; странице больше не важно как
function useOrders(filter: { status: string; sort: string; page: number }) {
  const [orders, setOrders] = useState<Order[]>([]);
  useEffect(() => { /* fetch + retry, привязано к filter */ }, [filter.status, filter.page]);
  const visible = useMemo(() => sortBy(filterByStatus(orders, filter.status), filter.sort), [orders, filter]);
  return { visible, revenue: visible.reduce((s, o) => s + o.total, 0) };
}

Страница импортирует useOrders и перестаёт быть клиентом базы данных. Одна обязанность — один дом.

3

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

function OrdersPage() {
  const [filter, setFilter] = useState({ status: "all", sort: "date", page: 1 });
  const { visible, revenue } = useOrders(filter);
  return (
    <>
      <OrdersToolbar filter={filter} onChange={setFilter} />
      <OrdersTable rows={visible} />        {/* владеет своим состоянием выбора */}
      <OrdersSummary revenue={revenue} />
      <Pagination page={filter.page} onPage={(page) => setFilter((f) => ({ ...f, page }))} />
    </>
  );
}

Божественного компонента больше нет: 12-строчный оркестратор над четырьмя связными частями и одним хуком.

4

Проброс пропсов — это запах связанности, и самое лёгкое лекарство — передача JSX как children, а не контекст. Когда значение продето сквозь слои, которые его никогда не читают, эти слои привязаны к пропу, который их не касается; переименование или удаление трогает их все. Рефлекс — «подкинуть контекст». Но часто промежуточные слои — это раскладка, а раскладку можно вывернуть: пусть родитель, у которого есть значение, рендерит потребителя и передаёт его вниз как children, чтобы средние слои вообще не видели проп.

// проброс: user продет через Layout -> Sidebar, которые им не пользуются
<Layout user={user}><Sidebar user={user} /></Layout>

// лекарство композицией: средние слои берут children; родитель поставляет потребителя
function Layout({ children }: { children: React.ReactNode }) { return <div className="shell">{children}</div>; }
<Layout><Sidebar><UserBadge user={user} /></Sidebar></Layout>  // user никуда не пробрасывается

Контекст — для случаев, когда многим далёким компонентам нужно одно и то же низкочастотное значение (тема, текущий пользователь, локаль) — а не для перепрыгивания двух слоёв раскладки. Тянись к children первым; он не добавляет ни новых концепций, ни машинерии перерисовок.

Разбор примера

Божественный Dashboard разложен, и лекарство от проброса, которое не тянется к контексту. До: один компонент владеет загрузкой, состоянием фильтра, бизнес-расчётом и всей раскладкой, и пробрасывает currentUser на два слоя вниз к бейджу.

function Dashboard({ currentUser }: { currentUser: User }) {
  const [projects, setProjects] = useState<Project[]>([]);
  const [query, setQuery] = useState("");
  useEffect(() => { fetch("/api/projects").then(r => r.json()).then(setProjects); }, []);
  const visible = projects.filter(p => p.name.includes(query));
  const overBudget = visible.filter(p => p.spent > p.budget).length; // бизнес-правило
  return (
    <div>
      <Header><Nav currentUser={currentUser} /></Header>  {/* пробрасывает user через Header -> Nav */}
      <input value={query} onChange={e => setQuery(e.target.value)} />
      <ProjectList projects={visible} />
      <p>{overBudget} over budget</p>
    </div>
  );
}

После: поведение переезжает в useProjects, слои раскладки берут children, так что currentUser больше не пробрасывается, и каждая часть связна.

function useProjects() {
  const [projects, setProjects] = useState<Project[]>([]);
  const [query, setQuery] = useState("");
  useEffect(() => { fetch("/api/projects").then(r => r.json()).then(setProjects); }, []);
  const visible = useMemo(() => projects.filter(p => p.name.includes(query)), [projects, query]);
  return { visible, query, setQuery, overBudget: visible.filter(p => p.spent > p.budget).length };
}

function Dashboard({ currentUser }: { currentUser: User }) {
  const { visible, query, setQuery, overBudget } = useProjects();
  return (
    <DashboardShell header={<Header><Nav><UserBadge user={currentUser} /></Nav></Header>}>
      <ProjectFilter query={query} onChange={setQuery} />
      <ProjectList projects={visible} />
      <BudgetSummary overBudget={overBudget} />
    </DashboardShell>
  );
}

Заметь, что мы не добавили: ни UserContext, ни стора Zustand. currentUser доходит до бейджа композицией — родитель, у которого он есть, рендерит потребителя и передаёт его как children. Мы добавили хук и разделили компоненты; мы добавили ноль глобального состояния. Эта сдержанность и есть ход уровня senior.

Частая ошибка

Классическая перекоррекция: кто-то видит user, проброшенный на три слоя, подкидывает UserProvider в корень приложения и считает это починкой. Теперь значение глобально, провайдер перерисовывает широкое поддерево всякий раз, когда объект пользователя меняется, поток данных невидим (его не проследить, читая JSX), а тестирование любого потребителя требует смонтированного провайдера. И всё это — чтобы не передавать children. Контекст и глобальные сторы — реальные инструменты, для по-настоящему разделяемого, многочитательского, низкочастотного состояния. Использовать их, чтобы увернуться от двух слоёв раскладки, — значит обменять маленькую, локальную, видимую связанность на большую, глобальную, невидимую. Это не починка; это запах похуже в архитектурном костюме.

Граничные случаи

Когда же контекст — правильное лекарство от проброса? Когда значение читается многими компонентами, разбросанными по дереву на разной глубине, меняется редко, а продевание его через children означало бы перестройку больших кусков раскладки, которые ты иначе трогать не хочешь — тема, локаль, аутентифицированный пользователь, потребляемый в пятнадцати местах, настройка плотности дизайн-системы. Тест не «это пробрасывается?», а «потребует ли композиция выворачивания дерева компонентов?». Если children ложится чисто — используй его. Если передача JSX вниз навязала бы неестественную форму многим точкам вызова, контекст (или редьюсер + контекст для разделяемых записей) окупает свою цену. Senior-суждение — выбирать по этому давлению, а не по рефлексу.

Проверь себя
Викторина

В PR `theme` (используется одним глубоким `<ThemedButton>`) продето через три чистые обёртки-раскладки через пропсы. Автор предлагает добавить глобальный контекст `ThemeProvider` в корень приложения, чтобы убрать проброс. Тема используется ровно в этом единственном месте. Каков senior-выбор?

Итог

Божественный компонент — это форма низкой связности в React: один блок владеет загрузкой, состоянием, бизнес-логикой и раскладкой, поэтому у него много причин меняться и большой радиус поражения. Проброс пропсов — это связанность, которую порождают божественные компоненты и высокие раскладки, — значения, продетые через слои, которые ими не пользуются. Лекарства — паттерны, которые ты уже знаешь: вынеси поведение в кастомные хуки, раздели разметку композицией и колоцируй состояние с владеющей им частью, превращая 500-строчный божественный компонент в тонкий оркестратор над сфокусированными частями. Для проброса подбирай лекарство под давление: передавай JSX как children, когда промежуточные слои — это просто раскладка (легчайшее, ноль новых концепций), поднимай состояние для соседей и тянись к контексту только когда многие далёкие компоненты читают одно и то же низкочастотное значение, а композиция вывернула бы дерево. Senior-режим отказа, которого надо избегать, — перекоррекция: подкинуть контекст на всё приложение или глобальный стор ради двух слоёв раскладки — значит обменять маленькую видимую связанность на большую невидимую. Распознай запах, а затем выбери самое лёгкое лекарство, которого давление действительно требует.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 4 завершено

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.