React
March 22, 2022

Возможно, вы не все знаете о useState...

Не ожидайте от этой статьи громкого разоблачения хука useState, но вот 7 фактов, которые стоило бы знать всем, кто собирается использовать хуки в react, я вам расскажу!

1. Функция обновления состояния имеет постоянную ссылку

Функция обновления стейта (второй элемент массива) - это одна и та же функция при каждом рендеринге. Вам не нужно включать его в зависимости массива, независимо от того, что eslint-plugin-react-hooks говорит по этому поводу:

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

2. При установке одинакового состояния ничего не происходит

useState - по умолчанию является чистой функцией. Если мы меняем значение стейта на его предыдущее значение, то ничего не происходит - нет обновления 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 });
};

3. Оптимизация инициализации стейта

Если вы переживаете за то что каждый раз будет создаваться стартовый объект инициализации, то можно использовать функцию:

const [style, setStyle] = useState(() => ({
    transform: props.isOpen ? null : 'translateX(-100%)',
    opacity: 0
}));Честно говоря, для меня это выглядит как чрезмерная оптимизация - зачем беспокоиться об одном объекте? Это может помочь с тяжелой логикой инициализации, но я еще не видела такого случая.

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

useState(() => () => console.log('Hello, traveler!'))

4. Обновление состояния при помощи колбека

Колбеки можно также использовать для того, чтобы обновлять состояние компонента - что-то вроде мини-редьюсера, у которого нет действия.

Это может быть полезно, когда текущее значение может не совпадать с итоговым значением, такое возможно если вы обновили значение с момента предыдущего рендера / мемоизации. Думаю, проще будет понять на примере:

const [clicks, setClicks] = useState(0);

const onMouseDown = () => {
    // Не будет работать, т.к clicks не изменился
    setClicks(clicks + 1);
    setClicks(clicks + 1);
};

const onMouseUp = () => {
    // А вот это будет, т.к мы прочитаем значение кликов в колбеке
    setClicks(clicks + 1);
    setClicks(clicks => clicks + 1);
};

Вобще, создание колбеков с постоянными ссылками более практично:

const [isDown, setIsDown] = useState(false);

// Плохой вариант, изменяется каждый раз при изменении isDown
const onClick = useCallback(() => setIsDown(!isDown), [isDown]);

// Отличный вариант, никогда не изменяется
const onClick = useCallback(() => setIsDown(v => !v), []);

5. Обновление состояния возвращает undefined

Это значит, что useState можно вернуть из хуков по типу useEffect без генерации предупреждения с таким текстом:

Warning: An effect function must not return anything besides a function, which is used for clean-up.

Вот эти фрагменты кода будут работать одинаково:

const [isOpen, setOpen] = useState(props.initOpen);

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

6. useState это по факту useReducer

Фактически, useState реализован в коде React как useReducer, только с заранее определенным редуктором (по крайней мере, начиная с 17.0)

Да, да, я правда люблю иногда покопаться в исходниках react...

В целом, если кто-то утверждает, что у useReducer есть множество серьезных преимуществ перед useState - то человека можно смело отправить копаться в исходниках (но помните, что это жестоко)!

7. Одно обновление состояния === один рендер в асинхронном коде

В react есть функция, называемая batching (обработка всего пакетами или пакетирование, как вам будет угодно), которая заставляет несколько вызовов setState вызывать один рендеринг, но она не всегда включена.

Давайте рассмотрим пример кода:

console.log('отрисовочка');

const [clicks, setClicks] = useState(0);
const [isDown, setIsDown] = useState(false);

const onClick = () => {
    setClicks(clicks + 1);
    setIsDown(!isDown);
};

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

  • <button onClick={onClick}> - пакетируется как обработчик событий React
  • useEffect(onClick, []) - тоже пакетируется
  • setTimeout(onClick, 10) - не пакетируется и вызовет дополнительную перерисовку
  • el.addEventListener('click', onClick) - тоже не пакетируется

Интересно

Но это собираются изменить в React 18, прочитать про это можно тут.

А тем временем вы можете использовать unstable_batchedUpdates для вызова принудительной пакетной обработки.

Надеюсь вы узнали для себя что-то новое!

А если изучение тайных знаний react - интересная для вас тема, то обязательно подписывайтесь на телеграм канал, чтобы не пропустить новые статьи!

Читать дальше: