Разработка с Владимиром

useState: 7 интересных фактов

Я захожу посмотреть код-ревью проекта с хуками. О нет! Вы опять забыли про классные фичи (и опасные ловушки), спрятанные в useState. Но не беспокойтесь, сейчас я расскажу вам все, что нужно знать про этот хук, кроме “аыа, там стейт можно обновить”. Никаких невероятных откровений, просто 7 фактов о useState, которые пригодятся каждому реакт-разработчику.

У функции обновления постоянная ссылка

Начнем с простого: функция обновления (вторая штучка в массиве) совершенно стабильна и не меняется между рендерами. Что бы ни думал на этот счет официальный хук-плагин для eslint, эту функцию не обязательно включать в зависимости других хуков (а если и включить, ничего плохого не случится):

const [count, setCount] = useState(0);
const onChange = useCallback((e) => {
    // setCount не меняется, и onChange тоже не будет
    setCount(Number(e.target.value));
}, []);

Обновление текущим значением не вызовет рендер

useStateчистая функция в реакт-смысле. Если новый стейт равен текущему (Object.is), реакт заметит это и ничего не сделает — не обновит DOM, не вызовет функцию рендера, вообще ничего. Совершенно незачем описывать эту логику еще раз:

const [isOpen, setOpen] = useState(props.initOpen);
const onClick = () => {
    // useState и сам это умеет
    if (!isOpen) {
        setOpen(true);
    }
};

Обратите внимание, что объекты сравниваются по ссылке, так что новый литерал вызовет рендер:

const [{ isOpen }, setState] = useState({ isOpen: true });
const onClick = () => {
    // Перерендериваем, потому что у объекта новая ссылка
    setState({ isOpen: false });
};

Функция обновления ничего не возвращает

А значит, setState можно вызвать в стрелке, и никаких ворнингов про An effect function must not return anything besides a function, which is used for clean-up не вылетит. Между этими вариантами нет никакой разницы:

useLayoutEffect(() => {
    setOpen(true);
}, []);
useLayoutEffect(() => setOpen(true), []);

useState и useReducer — одно и то же

Внутри реакта useState реализован через useReducer с прошитым редьюсером. Не верите — можете сами посмотреть в исходниках реакта. Если кто-то убеждает вас, что useReducer принципиально лучше useState — стабильнее, предсказуемее, оптимальнее, транзакционее — перед вами врунишка, который не знает о чем говорит.

Стейт можно инициализировать колбеком

Мучает совесть из-за того, что на каждый рендер вы создаете объект-инициализатор и тут же его выбрасываете (все уже и так инициализировано)? Попробуйте функцию-“ленивый инициализатор”:

const [style, setStyle] = useState(() => ({
    transform: props.isOpen ? null : 'translateX(-100%)',
    opacity: 0
}));

Инициализатор все еще может зависеть от пропов (или от другого стейта, или от чего угодно в скоупе рендера).Сомневаюсь, что это сильно ускорит приложение (на пару строк ниже мы все равно насоздаем виртуального дома как не в себя), но избавиться от ненужной работы всегда приятно. Возможно, у вас особо тяжелая логика вычисления исходного значения, но такого я, признаться, еще не видал.

Следствие 1: чтобы положить в стейт функцию (а почему бы нет?), ее придется обернуть в еще одну функцию, потому что реакт не различает функцию-которую-нужно-положить-в-стейт и инициализатор-который-нужно-вызвать. Вот так: useState(() => () => console.log('уф'))

Обновлять стейт тоже можно колбеком

Кроме явного значения функция обновления принимает колбеки. Получается такой инлайн-редьюсер, только без экшна. Теперь можно обновить стейт несколько раз в одной функции — ведь после первого обновления значение в скоупе протухло. Пример:

const [clicks, setClicks] = useState(0);
const onMouseDown = () => {
    setClicks(clicks + 1);
    // так не выйдет, потому что clicks в скоупе не изменился
    setClicks(clicks + 1);
};
const onMouseUp = () => {
    setClicks(clicks + 1);
    // а вот так мы получаем актуальное значение
    setClicks(clicks => clicks + 1);
};

Обычно я использую такую запись, чтобы стабилизировать листенер:

const [isDown, setIsDown] = useState(false);
// отстой, пересоздаем после каждого изменения
const onClick = useCallback(() => setIsDown(!isDown), [isDown]);
// ништяк, полностью стабильный колбек
const onClick = useCallback(() => setIsDown(v => !v), []);

Одно обновление стейта = один рендер — или нет?

Реакт умеет батчить обновления — группировать несколько обновлений от setState в один рендер. Но до 18 версии батчинг работает не везде. Посмотрите на этот код:

console.log('render');
const [clicks, setClicks] = useState(0);
const [isDown, setIsDown] = useState(false);
const onClick = () => {
    setClicks(clicks + 1);
    setIsDown(!isDown);
};

Количество рендеров при вызове onClick зависит от того, как именно мы его вызовем (я приготовил сандбоксик):

  • <button onClick={onClick}> батчится, потому что это реакт-листенер
  • useEffect(onClick, []) тоже батчится
  • setTimeout(onClick, 100) не батчится, рендерим 2 раза
  • el.addEventListener('click', onClick) тоже не батчится

React@18+ батчит более агрессивно. Если вы еще не обновились, то на помощь спешит, кхе-кхе, unstable_batchedUpdates (многие уважаемые люди используют его, стесняться нечего).


Теперь все вместе:

  • setState в [state, setState] = useState() — стабильная функция, не меняется при рендере
  • setState(текущее значение) и так ничего не делает, выкиньте свои if (значение !== текущее значение)
  • setState ничего не возвращает, так что useEffect(() => setState(true)) не ломает очистку эффекта
  • useState реализуется внутри реакта как useReducer с прошитым редьюсером
  • Стейт можно инициализировать колбеком: useState(() => initialValue)
  • Стейт можно обновить колбеком: setState(v => !v). Полезно для стабилизации useCallback.
  • Реакт до v18 батчит обновления от нескольких setState из реакт-листенеров (onChange=*) и эффектов, но не из DOM-листенеров (addEventListener) или асинхронных функций.

Надеюсь, вы узнали что-то новое и полезное! Иф ю кен ин инглиш, у меня еще много интересных статей. Если нет — подписывайтесь на меня, перевожу как могу. Всех обнимаю.

Эту статью написал ваш друг Владимир в году. Подписывайтесь на мой твиттер, чтобы узнать больше про веб-разработку. Или на RSS.