Проброс пропсов и когда он нормален
Проброс пропсов — это передача пропа через компоненты, которые им не пользуются. Два-три уровня часто нормальны: явно и поддаётся grep. Тянись к передаче children, затем к контексту, только когда проброс реально начинает мешать.
Ты передаёшь user в <Page>, который передаёт его в <Layout>, который передаёт в <Sidebar>, который наконец вручает его <Avatar>. Три компонента посередине потрогали проп, который никогда не читали. Это проброс пропсов (prop drilling), и в момент, когда джуниор видит такое, он тянется к контексту — потому что интернет сказал ему, что проброс это code smell.
Интернет прав наполовину. Проброс может сгноить кодовую базу. Но senior читает ту же трёхуровневую цепочку и очень часто просто пожимает плечами: это явно, это ищется через grep, в нём нет магии и его всё равно вот-вот отрефакторят. Навык не в том, чтобы устранять проброс, — а в том, чтобы знать порог, за которым он перестаёт быть нормальным, и тянуться сначала к самому дешёвому лекарству.
После этого урока ты можешь точно определить проброс пропсов (проп, переданный через компоненты, которые его не потребляют); судить, когда проброс нормален (мелкий, стабильный, явный), а когда он реально мешает (глубокий, текучий, шум); устранять проброс композицией — передавая children, чтобы уровень был пропущен, — прежде чем тянуться к контексту; и назвать режим отказа от слишком раннего прыжка к контексту: вынос в контекст состояния, которое разделяют всего два близких компонента.
Проброс пропсов — это передача пропа через промежуточные компоненты, которые им не пользуются, а лишь пробрасывают дальше. Цена не в самой глубине; цена в том, что каждый средний компонент теперь упоминает проп, о котором ему незачем знать, поэтому его сигнатура шире, а переименование расходится волной по файлам, которым это безразлично.
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 — когда его становится достаточно.
Два-три уровня часто нормальны — не плати за их устранение. Мелкий, стабильный проброс — самая честная форма потока данных, которую предлагает React: ты можешь проследить, откуда именно берётся значение, прочитав JSX, grep находит каждого потребителя, и нет ни косвенности, ни скрытой подписки. Контекст же, напротив, прячет проводку и добавляет канал перерисовки. Для пропа, который пробрасывается на два уровня и редко меняет форму, ретрансляция дешевле любого лекарства.
// Это нормально. Оставь как есть.
<Card>
<CardHeader title={title} /> {/* один уровень */}
</Card>
function CardHeader({ title }: { title: string }) {
return <h3>{title}</h3>;
}Тянуться к контексту здесь не делает код лучше — он меняет видимый, типизированный проп на невидимую глобальную переменную. Инстинкт senior: мелкий проброс — не проблема, пока не пересечён порог.
Проброс становится настоящим 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>Триггер к действию конкретен: посчитай уровни-только-ретрансляторы и связанные пропсы. Один-два чего-то из этого нормальны; четыре уровня, протягивающие четыре игнорируемых пропа, — сигнал к рефакторингу.
Тянись сначала к самому дешёвому лекарству: передаче 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-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.