React.js. Хук useCallback и зависимости
15.09.2021
Теги: Frontend • Hook • JavaScript • React.js • Web-разработка • Компонент • Теория • Функция
Смотрю сейчас на Udemy видео-курс по хукам — столкнулся с непониманием, зачем хуку useCallback
нужны зависимости. Выяснил у преподавателя и решил записать, чтобы не наступать больше на эти грабли. Итак, есть два компонента — Parent
и Child
, в Parent
есть функция, обернутая в useCallback
, чтобы избежать лишних рендеров Child
.
import React, { useState, useCallback } from 'react'; import Child from './Child'; export default function Parent(props) { const [state, setState] = useState('раз'); const handleParentClick = () => { const newValue = Math.random() < 0.5 ? 'раз' : 'два'; if (state !== newValue) { console.log('Изменение состояния Parent'); setState(newValue); } }; const handleChildClick = useCallback(() => { console.log('Обработчик клика Child'); }, []); console.log('Рендер компонента Parent'); return ( <div> <h1>Родитель, состояние «{state}»</h1> <Child name="Ребенок" childClickHandler={handleChildClick} /> <button onClick={handleParentClick}>Попытка изменить состояние Parent</button> </div> ); }
import React from 'react'; const Child = React.memo((props) => { console.log('Рендер компонента Child'); return <h2 onClick={props.childClickHandler}>{props.name}</h2> }); export default Child;
Мы всегда передаем из Parent
в Child
через пропсы ссылку на одну и ту же функцию — благодаря хуку useCallback
. По этой причине мы увидим рендер компонента Child
только один раз — в момент монтирования. Когда происходит рендер Parent
— это не будет вызывать рендер Child
— так работает связка React.memo
и useCallback
.
Все работает хорошо, пока мы не обратимся внутри handleChildClick
к переменной state
— она почему-то всегда равна «раз».
const handleChildClick = useCallback(() => { console.log('Обработчик клика Child'); console.log('Значение пременной state', state); }, []);
При первом рендере компонента Parent
, в момент создания функции handleChildClick
, она получает ссылку на лексическое окружение, в котором она создается. А хук useCallback
сохранит ссылку на функцию handleChildClick
, которая имеет замыкание над областью видимости функции Parent
. Другими словами, при вызове функции handleChildClick
ей будут доступны переменные state
, setState
, handleParentClick
— какими они были в момент создания функции. Тут лучше прочитать у Ильи Кантора — как работает замыкание.
Поскольку useCallback
с пустым массивом зависимостей будет всегда возвращать одну и ту же функцию, эта функция будет при вызове всегда выдавать одно и то же значение state
. Это то самое значение «раз», какое было при первом рендере — и handleChildClick
будет всегда доставать это значение из замыкания.
Чтобы console.log(state)
показывал актуальное значение переменной state
— нужно передать массив зависимостей при вызове хука.
const handleChildClick = useCallback(() => { console.log('Обработчик клика Child'); console.log('Значение пременной state', state); }, [state]);
Теперь, когда state
изменяет значение, функция будет создана заново, получит ссылку на текущее лексическое окружение, и при вызове будет иметь доступ к актуальному значению state
через замыкание. Хук useCallback
будет возвращать ссылку на эту новую функцию до тех пор, пока state
снова не изменит свое значение.
При нажатии на кнопку «Попытка изменить состояние» — состояние Parent
изменяется с вероятностью 50 процентов. Когда состояние изменяется — это вызывает рендер компонента Child
, потому что ссылка, которую возвращает handleChildClick
, изменилась. Когда состояние не изменяется — рендера Child
не будет, потому что ссылка на функцию тоже остается без изменений.
Поиск: Hook • JavaScript • React.js • Web-разработка • Frontend • Компонент • Теория • Функция • Хук • useCallback • Замыкание