Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке prop drilling і як його уникнути». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Prop drilling** - передача пропсів через проміжні компоненти, яким вони не потрібні, щоб дістатись до глибоко вкладеного нащадка. ```tsx function App() { return <Parent user={user} />; } function Parent({ user }) { return <Child user={user} />; } // Parent не використовує user function Child({ user }) { return <p>{user.name}</p>; } ``` **Головне:** виправляй через Context API при 3+ рівнях, або стейт-менеджер (Zustand, Redux) для логіки всього додатку. Звичайні пропси для 1-2 рівнів - це нормально.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Prop drilling** - це коли дані передаються через кілька шарів компонентів як пропси, хоча проміжні компоненти їх не використовують. ## Теорія ### TL;DR - Аналогія: передаєш записку через п'ятьох людей, щоб дістатися до одного в кінці. Всі посередині тримають, але не читають. - Головна проблема: проміжні компоненти засмічуються пропсами, які їм не потрібні. Рефакторинг стає болючим. - Правило вибору: 1-2 рівні вглибину - нормально. Від 3 і більше - варто дивитись на Context або стейт-менеджер. - Context пропускає посередників повністю. Стейт-менеджери (Redux, Zustand) - для бізнес-логіки і складного стану. - Drilling створює зв'язність. Додав новий проп - оновлюй весь ланцюг. ### Швидкий приклад ```tsx // Prop drilling: user проходить через Parent, якому він не потрібен function App() { const user = { name: 'John' }; return <Parent user={user} />; // передаємо вниз } function Parent({ user }) { // отримує, але не використовує return <Child user={user} />; // вимушений пробрасувати } function Child({ user }) { return <p>Привіт, {user.name}!</p>; // нарешті використовує } ``` `Parent` не має жодної причини знати про `user`. Але знає - бо `Child` потребує. Це і є prop drilling. ### Чому це стає проблемою На 2 рівнях нікого не хвилює. На 4-5 рівнях перейменування пропа означає правки в кожному компоненті ланцюга. Додаєш новий feature flag, який потрібен `GrandChild` - і тепер оновлюєш `App`, `Layout`, `Sidebar`, `Panel` просто щоб протягнути один булевий. Реальна ціна - це зв'язність. Компоненти залежать структурно від пропсів, які навіть не використовують. Тестуєш один - доводиться мокати пропси, які йому байдужі. ### Коли що використовувати - 1-2 рівні вглибину: залишай пропси, явний потік даних - це перевага - Тема, локаль, дані авторизації в 3+ компонентах: Context API - Складний стан з async-діями або збереженням: Redux Toolkit або Zustand - Часті оновлення для багатьох споживачів: стейт-менеджер замість Context, щоб уникнути зайвих перерендерів ### Таблиця порівняння | Аспект | Prop Drilling | React Context | Redux/Zustand | |--------|---------------|---------------|---------------| | Потік даних | Ручний на кожному рівні | Provider огортає піддерево | Глобальний store + селектори | | Перерендери | Тільки прямі нащадки | Всі споживачі при зміні value | Контрольовані через `useSelector` | | Бойлерплейт | Немає спочатку, росте швидко | Provider + `useContext` | Дії/редюсери (простіше в RTK) | | Тестування | Мокати пропси просто | Потрібна обгортка Context | Ізоляція store | | Підходить для | Плоскі дерева (<3 рівні) | UI-стан, середні додатки | Великі додатки, async-логіка | ### Як React обробляє це всередині React будує дерево fiber зверху вниз. Prop drilling додає зайві дані до проміжних fiber-вузлів - кожен рівень ланцюга несе їх під час reconciliation. Context (контекст) працює інакше: він використовує спеціальний `Context` fiber, який кешує поточне значення і сповіщає лише споживачів через окрему чергу dispatcher. Компоненти, які не викликають `useContext`, пропускають оновлення повністю. ### Рефакторинг через Context ```tsx const CartContext = createContext(); function Dashboard() { const [cartTotal, setCartTotal] = useState(0); return ( <CartContext.Provider value={{ cartTotal, setCartTotal }}> <Header /> </CartContext.Provider> ); } function Header() { return <Sidebar />; // жодного пропа cartTotal } function Sidebar() { return <CartWidget />; // жодного пропа cartTotal } function CartWidget() { const { cartTotal } = useContext(CartContext); return <span>{cartTotal} товарів</span>; } ``` `Header` і `Sidebar` чисті. Вони не знають про існування `cartTotal`. `CartWidget` бере його напряму. ### Типові помилки **Забути пробросити новий проп під час рефакторингу** ```tsx // Додали newFeature в App, забули шлях до Child <Parent user={user} newFeature={true} /> function Parent({ user }) { return <Child user={user} />; // newFeature мовчки губиться } ``` `Child` отримує `undefined`. Це класичний баг при глибокому drilling. З Context споживачі самі оголошують що їм потрібно, тому додавання нового значення до провайдера не потребує змін у проміжних компонентах. **Один Context на весь додаток зверху** ```tsx <UserContext.Provider value={user}> <EntireApp /> </UserContext.Provider> ``` Кожен `useContext(UserContext)` у споживача спрацює при зміні `user`. В додатку з десятками споживачів це накопичується. Розташовуй провайдери ближче до того місця, де дані реально потрібні. **Відсутній defaultValue у createContext** ```tsx const Ctx = createContext(); // без значення за замовчуванням function Consumer() { const val = useContext(Ctx); // падає поза Provider } ``` Якщо `Consumer` рендериться поза Provider (в тестах, порталах, lazy-маршрутах) - runtime помилка. Виправлення: `createContext({})` або `createContext(null)` з перевіркою. **Об'єкт як value без мемоізації** ```tsx function App() { const [theme, setTheme] = useState('light'); return ( // новий об'єкт при кожному рендері = всі споживачі оновлюються <ThemeContext.Provider value={{ theme, setTheme }}> <Button /> <UnrelatedChart /> {/* оновлюється при кожному рендері App */} </ThemeContext.Provider> ); } ``` Новий reference при кожному рендері тригерить усіх споживачів. Огорни в `useMemo`: `const value = useMemo(() => ({ theme, setTheme }), [theme])`. За моїм досвідом, саме це найчастіше спливає на code review: хтось замінив drilling на Context, але продуктивність погіршала через немемоізований об'єкт у Provider. ### Де зустрічається в реальних проектах - Chakra UI: `<ChakraProvider theme={customTheme}>` передає тему глобально, жодних пропсів теми не потрібно - Material-UI: `<ThemeProvider>` для стилізації по всьому додатку, включно з модалами і дровер-панелями - Next.js app router: стан авторизації в Context Provider, який огортає layout-компоненти - Великі продакшн-додатки: Redux для бізнес-стану, Context для UI-теми і локалізації ### Питання на співбесіді **Q:** Покажи prop drilling в дереві App -> Layout -> Sidebar -> UserMenu, потім виправ через Context. **A:** Пробрасуємо `user` через кожен рівень як проп. Потім створюємо `UserContext`, огортаємо `Layout` в `UserContext.Provider`, і викликаємо `useContext(UserContext)` напряму в `UserMenu`. Layout і Sidebar стають чистими. **Q:** Коли Context викликає більше перерендерів ніж prop drilling? **A:** Коли value Provider - це новий об'єкт або масив при кожному рендері без `useMemo`. Всі споживачі проходять reconciliation навіть якщо дані логічно не змінились. Drilling оновлює лише прямих нащадків. **Q:** Context проти Redux для кошика покупок? **A:** Context підходить для простого кошика (локальний UI-стан). Redux варто розглянути коли потрібні збереження, синхронізація з сервером або складні дії на зразок застосування купонів з async-валідацією. **Q:** Як тестувати компонент з Context без drilling? **A:** Огортаємо компонент у Provider всередині тесту: `render(<CartContext.Provider value={mockCart}><CartWidget /></CartContext.Provider>)`. **Q:** Як concurrent mode в React 18+ впливає на prop drilling порівняно з Context? **A:** Drilling може блокувати suspense-межі, бо дані недоступні поки батько не відрендериться. Context з `useTransition` дозволяє React позначати оновлення як несрочні, тому UI залишається чуйним під час повільних рендерів. ## Приклади ### Базовий: вітання користувача без Context і з Context ```tsx // Проблема: user пробрасується через Layout, якому він не потрібен function App() { const user = { name: 'Alice', role: 'admin' }; return <Layout user={user} />; } function Layout({ user }) { // не використовує user return <Navbar user={user} />; } function Navbar({ user }) { return <span>Вітаємо, {user.name}</span>; } // Виправлення: Context const UserContext = createContext(null); function App() { const user = { name: 'Alice', role: 'admin' }; return ( <UserContext.Provider value={user}> <Layout /> </UserContext.Provider> ); } function Layout() { return <Navbar />; // жодного пропа user } function Navbar() { const user = useContext(UserContext); return <span>Вітаємо, {user.name}</span>; } ``` `Layout` повністю відокремлений від даних користувача. Додавання `role` або `email` не потребує жодних змін у `Layout`. ### Середній: дашборд e-commerce з Context для кошика ```tsx const CartContext = createContext(); function Dashboard() { const [cartTotal, setCartTotal] = useState(3); return ( <CartContext.Provider value={{ cartTotal, setCartTotal }}> <Header /> </CartContext.Provider> ); } function Header() { return ( <nav> <Logo /> <Sidebar /> </nav> ); } function Sidebar() { return <CartWidget />; } function CartWidget() { const { cartTotal } = useContext(CartContext); return <button>Кошик ({cartTotal})</button>; } ``` Додавання нового споживача кошика (наприклад, підсумок на сторінці оформлення) - просто виклик `useContext(CartContext)`. Жодних змін у пропсах нікуди. ### Просунутий: пастка перерендерів і виправлення через useMemo ```tsx // Тригерить перерендер ВСІХ споживачів при кожному рендері App const ThemeContext = createContext(); function App() { const [theme, setTheme] = useState('light'); return ( // новий об'єкт при кожному рендері = всі споживачі оновлюються <ThemeContext.Provider value={{ theme, setTheme }}> <Button /> <ExpensiveChart /> {/* оновлюється навіть при незв'язаних змінах стану */} </ThemeContext.Provider> ); } // Виправлення: мемоізуємо value function App() { const [theme, setTheme] = useState('light'); const value = useMemo(() => ({ theme, setTheme }), [theme]); return ( <ThemeContext.Provider value={value}> <Button /> <ExpensiveChart /> {/* тепер оновлюється тільки при зміні theme */} </ThemeContext.Provider> ); } ``` `setTheme` стабільний (той самий reference від `useState`), тому `useMemo` з `[theme]` створює новий об'єкт тільки при реальній зміні теми. `ExpensiveChart` перестає оновлюватись на зайвих рендерах.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.