Prop drilling это передача пропса через компоненты, которые его не используют, только чтобы его мог прочитать компонент ниже по дереву. Здесь user проходит из App через Page и Sidebar, чтобы добраться до Avatar, единственного компонента, который его читает.
Четыре уровня, и user важен только последнему. Console показывает, что Page и Sidebar выполняются ради значения, которое они никогда не показывают. Нажмите кнопку, и они выведут сообщения снова.
Почему это мешает
Два или три уровня это нормально. Prop drilling становится проблемой по мере роста дерева:
- Промежуточные компоненты несут пропсы, которые не используют. У
PageиSidebarтеперь есть пропсuserв сигнатуре, и их невозможно читать, не задаваясь вопросом, зачем он нужен. - Каждое изменение затрагивает каждый уровень. Добавьте
user.avatarUrlили второе значение вродеonLogout, и придётся править каждый компонент на пути, а не только два конца. - Перенос компонента рвёт цепочку. Перенесите
Avatarв другую ветку, и придётся протягиватьuserпо новому пути. - Пропс легко потерять. Забудьте передать его на одном уровне, и глубокий компонент получит
undefinedбез всякой ошибки на том уровне, где он потерялся.
Попробуйте: добавьте в App функцию onLogout, которую должен вызывать Avatar. Придётся править все четыре компонента.
Решение 1: композиция через children
Это решение часто пропускают. Вместо того чтобы передавать данные вниз, чтобы глубокий компонент рендерился с ними, пусть компонент, которому принадлежат данные, сам отрендерит глубокий компонент и передаст готовый элемент вниз как children (или любой другой пропс). Промежуточные уровни рендерят слот и никогда не видят user.
Page и Sidebar больше не знают, что пользователь существует. App создаёт Avatar там, где живёт user, и передаёт его вниз готовым элементом. Такие компоненты макета становятся переиспользуемыми, а добавление onLogout теперь меняет только App и Avatar. Оба по-прежнему выводят сообщения при каждом клике, потому что App при каждом рендере передаёт им новый элемент: композиция упрощает код, а не уменьшает число рендеров. Больше шаблонов со слотами на странице о children.
Композиция работает, когда компонент, которому принадлежат данные, может также решать, как выглядит глубокая часть. Она не помогает, когда глубокий компонент находится внутри библиотеки или структуры, которую владелец не рендерит.
Решение 2: контекст
Когда многим компонентам на разной глубине нужно одно и то же значение (тема, вошедший пользователь, текущий язык), положите его в контекст. Любой компонент под провайдером читает его через useContext, и ничего между ними не меняется.
Avatar и Greeting читают пользователя с двух разных глубин, а Page и Sidebar не принимают пропсов. Добавьте третьего читателя где угодно под Page, и он заработает, не трогая остальное. Цена: поток данных больше не виден в пропсах, а каждый читатель рендерится при изменении значения. Страница о useContext разбирает провайдеры, значения по умолчанию и эти повторные рендеры.
Решение 3: библиотека состояния
Для большого состояния приложения, которое читают многие компоненты и которое часто меняется (корзина, редактор документов, живые данные), библиотека состояния вроде Redux Toolkit, Zustand или Jotai позволяет каждому компоненту подписаться только на тот срез, который он использует. Это избавляет и от drilling, и от цены «каждый потребитель рендерится» одного большого контекста. Это также ещё одна зависимость и ещё один набор понятий, поэтому беритесь за неё, когда первых двух решений становится недостаточно, а не чтобы исправить пропс, который проходит три уровня.
// Zustand, for comparison (not available in the editor here)
const useCart = create((set) => ({
items: [],
add: (item) => set((s) => ({ items: [...s.items, item] })),
}));
function CartCount() {
const count = useCart((s) => s.items.length); // renders only when the count changes
return <span>{count}</span>;
}
Когда prop drilling это нормально
Передача пропсов это способ, которым текут данные в React, и самый простой для чтения шаблон: значение можно проследить сверху вниз, просто глядя на JSX. Пропс, который проходит через один или два компонента, использующих его часть, или через короткую цепочку тесно связанных компонентов, не проблема, которую нужно решать. Поднимите состояние до ближайшего общего родителя, передайте его вниз и меняйте подход, только когда промежуточные уровни начинают нести пропсы, которые им ни к чему.
Часто задаваемые вопросы
Что такое prop drilling в React?
Передача пропса вниз через несколько уровней компонентов, которые его не используют, только чтобы его мог прочитать компонент глубоко в дереве. Каждый промежуточный компонент принимает пропс и передаёт его дальше.
Prop drilling это плохо?
Само по себе нет. Передача значения на два или три уровня вниз явная и легко отслеживается. Проблемой это становится, когда многие уровни несут пропсы, которые никогда не используют, и каждое переименование или новое поле затрагивает файлы, которые к нему не имеют отношения.
Как избежать prop drilling?
Сначала попробуйте композицию: пусть родитель сам создаст глубокий компонент и передаст его вниз как children или другой пропс, тогда промежуточные уровни никогда не увидят данные. Если значение нужно многим компонентам на разной глубине, используйте контекст. Для большого часто меняющегося состояния рассмотрите библиотеку состояния.
Контекст единственное решение prop drilling?
Нет, и часто не лучшее. Композиция с children убирает drilling без добавления контекста и оставляет поток данных видимым в коде.