Props Drilling: проблема

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

Зачем нужно

Понимание props drilling важно для осознанного выбора инструментов state management. Не каждая передача через 2-3 уровня — проблема. Проблема начинается, когда промежуточные компоненты становятся «трубами» — они принимают и передают пропсы, не используя их сами. Это усложняет рефакторинг: при добавлении нового поля нужно менять сигнатуры всех промежуточных компонентов.

Где используется (сигналы проблемы)

  • Компонент принимает проп только чтобы передать его дочернему
  • 4+ уровня вложенности с одними и теми же данными
  • Любое изменение структуры данных требует правок в 5+ файлах
  • Промежуточные компоненты содержат пропсы с именами типа onDeepChildAction

Иллюстрация проблемы

// Props drilling: user и onLogout нужны только в UserMenu,
// но проходят через App → Layout → Header → Navigation → UserMenu

function App() {
  const [user, setUser] = useState(currentUser);

  const handleLogout = () => setUser(null);

  // user и onLogout передаются вниз через все уровни
  return <Layout user={user} onLogout={handleLogout} />;
}

function Layout({ user, onLogout }) {
  // Layout не использует user и onLogout — только передаёт дальше
  return (
    <div>
      <Header user={user} onLogout={onLogout} />
      <main>...</main>
    </div>
  );
}

function Header({ user, onLogout }) {
  // Header тоже только пробрасывает
  return (
    <header>
      <Navigation user={user} onLogout={onLogout} />
    </header>
  );
}

function Navigation({ user, onLogout }) {
  return <UserMenu user={user} onLogout={onLogout} />;
}

// Только здесь данные реально используются
function UserMenu({ user, onLogout }) {
  return (
    <div>
      <span>{user.name}</span>
      <button onClick={onLogout}>Выйти</button>
    </div>
  );
}

Решения

Context API (встроено в React)

const AuthContext = createContext(null);

function App() {
  const [user, setUser] = useState(currentUser);
  const handleLogout = () => setUser(null);

  return (
    // Обёртка устраняет props drilling
    <AuthContext.Provider value={{ user, onLogout: handleLogout }}>
      <Layout />  {/* Никаких лишних пропсов */}
    </AuthContext.Provider>
  );
}

function UserMenu() {
  // Получаем напрямую, пропуская промежуточные компоненты
  const { user, onLogout } = useContext(AuthContext);
  return (
    <div>
      <span>{user.name}</span>
      <button onClick={onLogout}>Выйти</button>
    </div>
  );
}

Composition (когда это подходит)

// Вместо пробрасывания данных — передаём готовый компонент
function App() {
  const [user, setUser] = useState(currentUser);

  return (
    <Layout
      // children уже содержит нужный компонентнет drilling
      header={<Header menu={<UserMenu user={user} />} />}
    />
  );
}

Когда НЕ нужно бороться с drilling

1-2 уровня → нормально, не трогать
3 уровня с реально используемыми пропсами → допустимо
4+ уровней с «пустыми» промежуточными компонентами → Context или composition
Часто меняющиеся данные на многих уровнях → Redux, Zustand

Частые ошибки

  • Context на всё подряд — Context для локальных данных, которые нужны 1-2 компонентам, создаёт ненужную сложность.
  • Принимают drilling как норму — 10 уровней передачи одного проп — явный сигнал о проблеме архитектуры.
  • Не рассматривают composition — часто drilling можно устранить через грамотную компоновку компонентов без Context.

Связанные темы

Ресурсы