Что такое "prop drilling" и как его можно избежать?

"Prop drilling" (буквально: «сверление пропсов») — это процесс передачи данных (props) от родительского компонента к глубоко вложенному дочернему компоненту через несколько промежуточных компонентов, которым эти данные на самом деле не нужны, но они должны их получать и передавать дальше, чтобы в итоге нужный компонент получил нужную информацию.

Пример prop drilling

Представим такую структуру компонентов:

<App>
<Parent>
<Child>
<Grandchild />
</Child>
</Parent>
</App>

Допустим, компонент Grandchild нуждается в данных user, которые находятся в App. Если передавать user через каждый уровень, это выглядит так:

function App() {
const user = { name: 'Alice' };
return <Parent user={user} />;
}
function Parent({ user }) {
return <Child user={user} />;
}
function Child({ user }) {
return <Grandchild user={user} />;
}
function Grandchild({ user }) {
return <p>{user.name}</p>;
}

Каждый промежуточный компонент (Parent, Child) обязан принимать user и передавать его дальше, хотя сам его не использует.

Проблемы prop drilling

  1. Сложность масштабирования — при увеличении глубины вложенности становится всё сложнее отслеживать, откуда приходят пропсы.

  2. Загрязнение интерфейсов — компоненты вынуждены принимать и передавать пропсы, которые им не нужны.

  3. Трудности поддержки — малейшие изменения в структуре данных требуют правок во всех промежуточных компонентах.

  4. Пониженная читаемость — трудно понять, какие компоненты реально используют данные.

  5. Избыточные рендеры — если изменился проп, через который проходят данные, все компоненты в цепочке могут перерендериться, даже если они его не используют.

Способы избежать prop drilling

1. React Context API

Контекст позволяет передавать данные от родительского компонента к любому вложенному, минуя промежуточные.

Пример:

const UserContext = React.createContext();
function App() {
const user = { name: 'Alice' };
return (
<UserContext.Provider value={user}>
<Parent />
</UserContext.Provider>
);
}
function Parent() {
return <Child />;
}
function Child() {
return <Grandchild />;
}
function Grandchild() {
const user = React.useContext(UserContext);
return <p>{user.name}</p>;
}

Компонент Grandchild получает user напрямую из контекста, без участия Parent и Child.

2. Менеджеры состояния (Redux, Zustand, MobX, Recoil и др.)

Более масштабные и централизованные подходы. Компоненты могут получать данные из общего хранилища.

Пример с Redux:

  • App предоставляет Redux store.

  • Компоненты, которым нужны данные, подписываются через useSelector.

  • Нет необходимости передавать данные через иерархию.

3. Композиция через children или render props

Иногда данные можно обернуть в компонент, который передаст их нужным компонентам через children.

function UserProvider({ user, children }) {
return children(user);
}
function App() {
const user = { name: 'Alice' };
return (
<UserProvider user={user}>
{(user) => <Grandchild user={user} />}
</UserProvider>
);
}

Или:

function App() {
const user = { name: 'Alice' };
return <GrandchildWrapper user={user} />;
}
function GrandchildWrapper({ user }) {
return <Grandchild user={user} />;
}

Это немного упрощает передачу пропсов в специфических случаях, хотя не решает проблему глобально.

4. React Router + useParams / useLocation

Если нужные данные могут быть закодированы в URL (например, id пользователя), их можно извлекать с помощью хука useParams, не передавая пропсы напрямую.

5. Custom Hooks

Можно спрятать логику доступа к данным в кастомный хук, который будет использовать, например, useContext или доступ к состоянию Redux.

function useUser() {
return React.useContext(UserContext);
}
А затем:
function Grandchild() {
const user = useUser();
return <p>{user.name}</p>;
}

Это упрощает повторное использование и делает код чище.

Когда prop drilling допустим

  • Когда компоненты неглубоко вложены.

  • Когда только 1–2 уровня между родителем и потребителем.

  • Когда данные используются только одним потомком, и он находится недалеко.

Отличие prop drilling от lifting state up

  • Lifting state up — это подход, когда состояние перемещается выше по иерархии, чтобы управлять им из одного места.

  • Prop drilling — это следствие необходимости передавать данные вниз по иерархии, когда нижние компоненты нуждаются в данных, доступных наверху.

Prop drilling vs Context vs Redux

Подход Когда использовать
Prop drilling Простые случаи, небольшая вложенность
--- ---
Context API Умеренно сложные приложения, редко изменяемые данные
--- ---
Redux/Zustand Сложные приложения, много состояний, асинхронность
--- ---