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

Проброс пропсов и когда он нормален

Проброс пропсов — это передача пропа через компоненты, которые им не пользуются. Два-три уровня часто нормальны: явно и поддаётся grep. Тянись к передаче children, затем к контексту, только когда проброс реально начинает мешать.

RXP Middle ◷ 18 min
Уровень
ОсновыJuniorMiddleSenior

Ты передаёшь user в <Page>, который передаёт его в <Layout>, который передаёт в <Sidebar>, который наконец вручает его <Avatar>. Три компонента посередине потрогали проп, который никогда не читали. Это проброс пропсов (prop drilling), и в момент, когда джуниор видит такое, он тянется к контексту — потому что интернет сказал ему, что проброс это code smell.

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

Цель

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

1

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

function Page({ user }: { user: User }) {
  return <Layout user={user} />;          // Layout не читает user
}
function Layout({ user }: { user: User }) {
  return <Sidebar user={user} />;         // Sidebar не читает user
}
function Sidebar({ user }: { user: User }) {
  return <Avatar user={user} />;          // наконец, потребитель
}

Layout и Sidebar — чистый транзит. Они получили проп user в свой тип и в свой JSX исключительно как ретранслятор. Этот ретранслятор и есть smell — когда его становится достаточно.

2

Два-три уровня часто нормальны — не плати за их устранение. Мелкий, стабильный проброс — самая честная форма потока данных, которую предлагает React: ты можешь проследить, откуда именно берётся значение, прочитав JSX, grep находит каждого потребителя, и нет ни косвенности, ни скрытой подписки. Контекст же, напротив, прячет проводку и добавляет канал перерисовки. Для пропа, который пробрасывается на два уровня и редко меняет форму, ретрансляция дешевле любого лекарства.

// Это нормально. Оставь как есть.
<Card>
  <CardHeader title={title} />     {/* один уровень */}
</Card>

function CardHeader({ title }: { title: string }) {
  return <h3>{title}</h3>;
}

Тянуться к контексту здесь не делает код лучше — он меняет видимый, типизированный проп на невидимую глобальную переменную. Инстинкт senior: мелкий проброс — не проблема, пока не пересечён порог.

3

Проброс становится настоящим smell за порогом: много уровней, много пропсов или текучесть. Боль накапливается, когда (а) цепочка глубокая — пять и больше ретрансляций; (б) ты протягиваешь связку пропсов, а не один; или (в) форма часто меняется, так что каждая правка снова трогает каждый средний компонент. Вот тогда налог на транзит вылезает в виде merge-конфликтов и «почему Layout принимает двенадцать пропсов, которые игнорирует?».

// Теперь это мешает: связка пропсов, протянутая глубоко, которую средние игнорируют.
<Layout user={user} theme={theme} locale={locale} onLogout={onLogout}>
  <Sidebar user={user} theme={theme} locale={locale} onLogout={onLogout}>
    <Nav user={user} theme={theme} locale={locale} onLogout={onLogout} />
  </Sidebar>
</Layout>

Триггер к действию конкретен: посчитай уровни-только-ретрансляторы и связанные пропсы. Один-два чего-то из этого нормальны; четыре уровня, протягивающие четыре игнорируемых пропа, — сигнал к рефакторингу.

4

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

// Лекарство композицией: Layout/Sidebar принимают children, никогда не видят `user`.
function Page({ user }: { user: User }) {
  return (
    <Layout>
      <Sidebar>
        <Avatar user={user} />   {/* владелец рендерит потребителя напрямую */}
      </Sidebar>
    </Layout>
  );
}
function Layout({ children }: { children: React.ReactNode }) {
  return <div className="layout">{children}</div>;
}

Layout и Sidebar полностью лишились пропа user. Ни контекста, ни библиотеки — просто перенос того, кто что рендерит. Это приём, который надо выучить до контекста: композиция убирает проброс, не добавляя канала подписки.

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

Реальная цепочка: проброшена, затем вылечена композицией — не контекстом. Дашборд протягивает залогиненного user и обработчик onSignOut вниз к аватарке в шапке на четыре уровня вглубь. Каждый средний компонент — транзит.

До — проброс связки через оболочки, которые её игнорируют:

function Dashboard({ user, onSignOut }: { user: User; onSignOut: () => void }) {
  return <Shell user={user} onSignOut={onSignOut} />;
}
function Shell({ user, onSignOut }: Props) {
  return <TopBar user={user} onSignOut={onSignOut} />;   // игнорирует оба
}
function TopBar({ user, onSignOut }: Props) {
  return <UserMenu user={user} onSignOut={onSignOut} />; // игнорирует оба
}
function UserMenu({ user, onSignOut }: Props) {          // единственный потребитель
  return <button onClick={onSignOut}>{user.name}</button>;
}

Shell и TopBar существуют ради раскладки; они принимают user и onSignOut только чтобы ретранслировать их. Четыре сигнатуры несут пропсы, которые две из них никогда не читают.

После — владелец рендерит потребителя и передаёт его как children:

function Dashboard({ user, onSignOut }: { user: User; onSignOut: () => void }) {
  return (
    <Shell>
      <TopBar>
        <UserMenu user={user} onSignOut={onSignOut} />  {/* срендерено здесь */}
      </TopBar>
    </Shell>
  );
}
function Shell({ children }: { children: React.ReactNode }) {
  return <div className="shell">{children}</div>;       // без пропа user
}
function TopBar({ children }: { children: React.ReactNode }) {
  return <header>{children}</header>;                    // без пропа user
}

Проброс исчез. Shell и TopBar теперь — переиспользуемые раскладочные оболочки с одной задачей, а user/onSignOut едут напрямую от владельца к потребителю. Заметь, что мы не создавали UserContext — потребитель ровно один, и он рендерится рядом из владельца, так что контекст был бы ценой без выгоды. Композиция — лекарство правильного размера; контекст остаётся в инструментах для случая, когда потребителей много и они далеки.

Почему это работает

Почему передача children строго дешевле контекста в этом случае? Потому что children не добавляет ни одного нового ребра зависимости — значение по-прежнему течёт сверху вниз через единственного владельца, полностью видимое и типизированное, а средние компоненты становятся искренне о нём неосведомлёнными (лучше, а не просто короче). Контекст же вводит подписку: любой компонент, вызывающий useContext, перерисовывается при изменении значения провайдера, а источник данных теперь невидим в точке вызова. Оба убирают ретрансляцию, но только children убирает её без размена явности на скрытый канал. Поэтому порядок намеренный: сначала композиция, контекст — лишь когда дистанция и число потребителей делают композицию непрактичной.

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

Классический отказ через переусложнение: вынос в контекст состояния, которое разделяют всего два близких компонента. Поле поиска и список результатов стоят бок о бок под одним родителем; поле владеет запросом, список его читает. Джуниор, у которого аллергия на один проп между ними, оборачивает пару в SearchContext. Теперь общее состояние имеет глобальную форму, каждый потребитель useContext перерисовывается на каждое нажатие клавиши, источник скрыт, и ты добавил провайдер ради двух компонентов, которым достаточно было поднять состояние в общего родителя и передать один проп. Контекст — для широко разделяемого, относительно стабильного состояния, читаемого многими далёкими компонентами, — не для двух соседей. Когда состояние разделяют всего два близких компонента, подними его в их родителя и передай проп. Тянуться к контексту с первого же прохода — то самое переусложнение, от которого этот трек не устаёт предостерегать.

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

Значение залогиненного user проброшено ровно на два уровня (Page → Layout → Avatar) и редко меняет форму. Коллега хочет заменить проброс провайдером UserContext. Каков выбор уровня senior?

Итог

Проброс пропсов — это передача пропа через промежуточные компоненты, которые лишь ретранслируют его. Это не автоматически smell: два-три стабильных уровня часто нормальны — явно, типизированно, ищется через grep, без магии — и их устранение может стоить дороже, чем экономит. Проброс превращается в настоящую проблему только за порогом: глубокие цепочки, связки ретранслируемых пропсов или формы, которые текут. Когда ты всё же лечишь его, тянись к самому дешёвому средству первым — передай children, чтобы владелец рендерил потребителя напрямую, а средние компоненты стали неосведомлёнными раскладочными оболочками, убирая ретрансляцию без добавления подписки. Контекст — крайнее средство, оправданное лишь когда потребителей много, они далеки и их нельзя срендерить рядом. Фирменный режим отказа — прыжок прямо к контексту, особенно вынос в контекст состояния, которое разделяют всего два близких компонента, что меняет один честный проп на скрытую глобальную переменную и ненужные перерисовки. Подбирай лекарство под боль: большинству пробросов контекст вовсе не нужен.

Практика

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

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

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

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

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

Trademarks belong to their respective owners. Editorial reference only.