Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Порядок рендерингу компонентів та виклику хуків у React». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**Порядок рендерингу компонентів у React** - React обходить дерево у глибину: спочатку батько, потім рекурсивно діти, потім сусіди. ```jsx // App -> Header -> Main -> Sidebar -> Content -> Footer const [count, setCount] = useState(0); // Хук 1 - завжди позиція 0 const ref = useRef(null); // Хук 2 - завжди позиція 1 ``` **Ключове:** React ідентифікує хуки за індексом виклику у зв'язаному списку файбера, а не за назвою - тому порядок хуків не може змінюватись між рендерами.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**Порядок рендерингу компонентів у React** - React обходить дерево компонентів у глибину при кожному оновленні, викликаючи кожну функцію компонента зверху вниз, а хуки всередині кожного компонента мають виконуватись у точно такому ж порядку при кожному рендері. ## Теорія ### TL;DR - Рендеринг схожий на сімейну вечерю: батько сідає за стіл першим, потім кличе дітей по одному, і тільки після них приходять сусіди по столу - React рендерить батька, іде вглиб до дітей, потім переходить до сусідів - Хуки ідентифікуються за позицією виклику у зв'язаному списку кожного компонента, а не за назвою - Якщо кількість хуків змінюється між рендерами, React видає помилку - Умовна логіка йде всередину хука, а не навколо нього ### Швидкий приклад ```jsx function Parent() { console.log('Parent render'); // 1-й return <Child />; } function Child() { console.log('Child render'); // 2-й const [count, setCount] = useState(0); // Хук 1 - завжди позиція 0 const ref = useRef(null); // Хук 2 - завжди позиція 1 useEffect(() => { console.log('Child effect'); // 4-й - після відмалювання браузером }, []); console.log('Child hooks done'); // 3-й return <div ref={ref}>{count}</div>; } // Вивід у консоль: // Parent render // Child render // Child hooks done // Child effect ``` Спочатку рендериться Parent, потім синхронно Child, потім ефекти виконуються після відмалювання браузером. Ця послідовність фіксована. ### Порядок рендерингу: обхід у глибину React обходить JSX-дерево зверху вниз. Для такої структури: ```jsx function App() { return ( <> <SiblingA /> <Parent /> <SiblingB /> </> ); } function Parent() { return <><ChildA /><ChildB /></>; } function ChildA() { console.log('ChildA'); return null; } function ChildB() { console.log('ChildB'); return null; } function SiblingA() { console.log('SiblingA'); return null; } function SiblingB() { console.log('SiblingB'); return null; } // Вивід: App -> SiblingA -> Parent -> ChildA -> ChildB -> SiblingB ``` React іде вглиб кожного піддерева перед переходом до наступного сусіда. `SiblingB` не починає рендеритись, доки не завершиться все піддерево `Parent`. Це важливо при налагодженні. Якщо `ChildA` повільний, він блокує `ChildB` і `SiblingB`. `console.log` на початку кожного компонента показує цю послідовність точно. На практиці цей прийом виявляє більше несподіваних ре-рендерів, ніж більшість сесій профілювання. ### Як React відстежує стан хуків Кожен компонент у Fiber-архітектурі має свій зв'язаний список станів хуків. Три хуки в компоненті означає, що React зберігає їх на позиціях 0, 1 і 2 у цьому списку. При повторному рендері React відтворює ці виклики в тому ж порядку і читає позиції 0, 1, 2, щоб отримати попередні значення. Жодних назв. Жодної магії. Тільки індекс позиції. Саме тому порядок хуків не може змінюватись. Пропущений хук 1 переміщує хук 2 на позицію 0. React читає неправильний стан. Баг часто непомітний, поки щось не зламається в продакшені. ### Фази виконання хуків Хуки поділяються на дві групи за часом виконання. **Під час рендеру:** `useState`, `useReducer`, `useContext`, `useRef`, `useMemo`, `useCallback`. Виконуються синхронно під час виклику функції компонента, в порядку оголошення. **Після рендеру:** `useLayoutEffect` виконується після оновлення DOM, але до відмалювання браузером. Використовуй його, коли потрібно виміряти елемент або виправити розмітку до того, як користувач побачить екран. `useEffect` виконується після відмалювання браузером. Це правильне місце для запитів даних, підписок і таймерів. Функції очищення (cleanup) виконуються у зворотному порядку при розмонтуванні: спочатку очищаються діти, потім батько. ### Коли це знання потрібне - **Налагодження зайвих ре-рендерів:** додай `console.log` на початку кожного компонента, перевір, які з них спрацьовують без потреби - **Кастомні хуки:** завжди викликай їх на верхньому рівні, ніколи всередині циклів або умов - кастомний хук це функція що викликає інші хуки, ті самі правила - **Продуктивність:** обгортай піддерева в `React.memo`, щоб ре-рендер батька не каскадував у дітей, які не залежать від зміненого стану - **`useLayoutEffect` проти `useEffect`:** якщо бачиш мерехтіння при оновленні розміру або позиції елемента, переходь з `useEffect` на `useLayoutEffect` - це запобігає відмалюванню браузером проміжного стану ### Типові помилки **Умовний виклик хука:** ```jsx // Неправильно function Bad({ show }) { if (show) { const [count, setCount] = useState(0); // пропускається коли show false } return <div />; } // Правильно function Good({ show }) { const [count, setCount] = useState(0); // викликається завжди return show ? <div>{count}</div> : null; } ``` Коли `show` змінюється з `true` на `false`, хук на позиції 0 зникає. React читає значення наступного хука на неправильне місце. Результат - неправильний стан або помилка "Rendered fewer hooks than expected". **Хук всередині циклу:** ```jsx // Неправильно function BadList({ items }) { items.forEach(item => { const [state, setState] = useState(0); // кількість змінюється з items.length }); } // Правильно function GoodList({ items }) { const [states, setStates] = useState(() => items.map(() => 0)); // один useState тримає весь масив } ``` React видає "Rendered more hooks than during the previous render", коли змінюється `items.length`. Один `useState` з масивом вирішує проблему. **Ранній return перед хуками:** ```jsx // Неправильно function Bad({ user }) { if (!user) return null; // хуки нижче не викликаються при цьому рендері const [active, setActive] = useState(false); return <div>{active}</div>; } // Правильно function Good({ user }) { const [active, setActive] = useState(false); // виконується завжди if (!user) return null; return <div>{active}</div>; } ``` **Умова всередині кастомного хука:** ```jsx // Неправильно function useData(id) { if (!id) return null; const [data, setData] = useState(null); // умовний виклик всередині хука } // Правильно function useData(id) { const [data, setData] = useState(null); useEffect(() => { if (id) fetchData(id).then(setData); }, [id]); return data; } ``` ### Де зустрічається в реальних проектах - **Next.js:** серверний рендеринг і гідратація залежать від збігу порядку рендерингу між сервером і клієнтом. Розбіжність у хуках спричиняє помилки гідратації, які важко відстежити - **TanStack Query:** `useQuery` треба викликати на верхньому рівні - ключ кешу залежить від стабільної позиції виклику - **Redux Toolkit:** `useSelector` у підключених компонентах підпорядковується тому ж механізму зв'язаного списку - **React DevTools Profiler:** показує порядок і час рендерингу кожного компонента в дереві - **Concurrent React 18:** фаза рендеру залишається синхронною і обходить дерево у глибину; фаза фіксації може поступатися для пріоритетної роботи, але порядок компонентів і хуків всередині одного проходу рендеру незмінний ### Follow-up питання **Q:** Який порядок рендерингу для `<Parent><A/><B/></Parent><Sibling/>`? **A:** `Parent -> A -> B -> Sibling`. React завершує все піддерево `Parent` перед переходом до `Sibling`. **Q:** Чому не можна викликати `useState` всередині `if`? **A:** React ідентифікує хуки за індексом виклику у зв'язаному списку файбера. Пропуск виклику зміщує всі наступні індекси, і React читає неправильні значення при наступному рендері. **Q:** Який порядок очищення при розмонтуванні компонентів? **A:** Зворотний порядку монтування. Спочатку очищаються діти, потім батьки. Це відповідає порядку фіксації ефектів, знизу вгору. **Q:** Чим відрізняється `useLayoutEffect` від `useEffect` за часом виконання? **A:** `useLayoutEffect` виконується синхронно після мутації DOM, але до відмалювання браузером. `useEffect` виконується після відмалювання. Використовуй `useLayoutEffect`, коли потрібно читати або змінювати розміри DOM до того, як користувач побачить результат. **Q:** Чи може порядок рендерингу змінитися в concurrent mode React 18? **A:** Порядок обходу дерева у глибину залишається незмінним. Concurrent mode може призупиняти і відновлювати роботу рендеру за пріоритетом, але всередині одного проходу послідовність виклику компонентів і хуків ідентична попереднім версіям React. **Q (senior):** Компонент ре-рендериться зі скороченим списком. Хуки правильно оголошені поза циклом. Але обробники `useCallback` досі посилаються на старі значення елементів. Чому? **A:** Застарілі замикання (stale closures). `useCallback` захоплює змінні з рендеру, під час якого callback був останній раз створений. Якщо масив залежностей не включає змінені значення, callback тримає посилання на старий стан. Рішення - додати ці значення до масиву залежностей `useCallback`, або зберігати останнє значення в ref без тригеру ре-рендеру. ## Приклади ### Базовий: порядок рендерингу в дереві компонентів ```jsx function App() { console.log('App'); return ( <> <Header /> <Main /> <Footer /> </> ); } function Main() { console.log('Main'); return ( <> <Sidebar /> <Content /> </> ); } function Header() { console.log('Header'); return <header />; } function Sidebar() { console.log('Sidebar'); return <aside />; } function Content() { console.log('Content'); return <main />; } function Footer() { console.log('Footer'); return <footer />; } // Вивід: // App // Header // Main // Sidebar // Content // Footer ``` `Header` завершується до початку `Main`. `Sidebar` і `Content` обидва завершуються до початку `Footer`. Обхід у глибину: сусіди рендеряться після всіх дітей батька. ### Середній: хуки в реальному компоненті дашборду ```jsx function UserDashboard({ userId }) { // Всі хуки оголошуються безумовно на початку const [user, setUser] = useState(null); // Хук 1: завжди позиція 0 const [stats, setStats] = useState({}); // Хук 2: завжди позиція 1 const containerRef = useRef(null); // Хук 3: завжди позиція 2 useEffect(() => { if (userId > 0) { fetchUser(userId).then(setUser); // умова всередині, а не навколо хука } }, [userId]); if (!userId) return null; // ранній return ПІСЛЯ всіх хуків return ( <div ref={containerRef}> <UserProfile user={user} /> <StatsPanel data={stats} /> </div> ); } // Порядок рендерингу: UserDashboard -> UserProfile -> StatsPanel // Порядок хуків при кожному рендері: useState(null), useState({}), useRef, useEffect ``` Всі три хуки виконуються при кожному рендері незалежно від `userId`. Умова переміщена всередину `useEffect`. Ранній return стоїть після хуків. Саме такий патерн очікує React. ### Просунутий: діагностика та виправлення помилки порядку хуків ```jsx // Помилка: кількість хуків змінюється разом з items.length function BadList({ items }) { const renderedItems = items.map(item => { const [selected, setSelected] = useState(false); // неправильно: різна кількість щоразу return ( <li key={item.id} onClick={() => setSelected(s => !s)}> {item.name} {selected ? '(вибрано)' : ''} </li> ); }); return <ul>{renderedItems}</ul>; } // Помилка React: "Rendered more hooks than during the previous render." // Рішення: один useReducer на верхньому рівні, стабільна кількість хуків function GoodList({ items }) { const [selected, dispatch] = useReducer( (state, id) => ({ ...state, [id]: !state[id] }), {} ); return ( <ul> {items.map(item => ( <li key={item.id} onClick={() => dispatch(item.id)}> {item.name} {selected[item.id] ? '(вибрано)' : ''} </li> ))} </ul> ); } ``` Переміщення стану кожного елемента в цикл порушує правило фіксованої кількості хуків. Один `useReducer` на верхньому рівні обробляє всі елементи, тримає кількість хуків стабільною на рівні 1 незалежно від розміру списку, і уникає краху при додаванні або видаленні елементів.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.