Можно подумать, что хук React useState — самый простой хук. Тем не менее, есть некоторые сложности.

Что такое пакетирование?

Пакетная обработка — это когда несколько вызовов setState объединяются только в одно обновление состояния.

function App() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);
  useEffect(() => {
    // only output once per click
    console.log({ count, flag });
  }, [count, flag]);
  const handleClick = () => {
    // Here, react will re-render only once
    // Hence, the state updates are `batched`
    setCount(c => c + 1);
    setFlag(f => !f);
  };
  return (
    <div className='App'>
      <button onClick={handleClick}>Click Me!</button>
      <h3 style={{ color: flag ? 'blue' : 'black' }}>Count: {count}</h3>
    </div>
  );
}

Почему дозирование?

  • Отлично подходит для производительности, поскольку позволяет избежать ненужных повторных рендеров.
  • Предотвращает рендеринг любым компонентом «наполовину примененных» обновлений состояния, что может привести к ошибкам (например, )

Непоследовательное поведение пакетной обработки

Однако React был (подробнее об этом позже) несовместимым с пакетной обработкой. Например, в асинхронной функции / API на основе обещаний React не будет пакетировать обновления, а будут происходить независимые обновления (выполнение двух вызовов setState).

// little async function
const sleep = () => new Promise(resolve => setTimeout(resolve, 200));
export default function App() {
  const [flag, setFlag] = useState(true);
  const [count, setCount] = useState(0);
  const handleClick = async () => {
    // mimicing some async call
    // (ex, fecthing data from server, etc.)
    await sleep();
    setFlag(f => !f);
    setCount(c => c + 1);
  };
  useEffect(() => {
    // in this case, two console logs can be seen
    // since `setState` is called inside an asynchronous function
    // So,  React would not batch the updates, and perform two independent updates.
    console.log({ count, flag });
    // whenever `flag` or `count` changes, do somethig!
  }, [count, flag]);
  return (
    <>
      <h2>React's Batching Behavior while inside async callbacks</h2>;
      <p>Count: {count}</p>
      <button
        onClick={handleClick}
        style={{ backgroundColor: flag ? 'orange' : 'blue', color: '#fff' }}
      >
        Click me!
      </button>
    </>
  );
}

Принудительная пакетная обработка в асинхронных функциях

Чтобы заставить setState выполнять пакетные обновления из обработчиков событий, можно использовать unstable_batchedUpdates (недокументированный API):

import { unstable_batchedUpdates } from 'react-dom';
unstable_batchedUpdates(() => {
  setCount(c => c + 1);
  setFlag(f => !f);
});

Это связано с тем, что раньше React выполнял пакетные обновления только во время события браузера (например, клика), но здесь мы обновляем состояние после того, как событие уже было обработано (в функции aync):

Для демонстрации см. React 17: принудительная пакетная обработка вне обработчиков событий

Отказаться от автоматической пакетной обработки

Некоторый код может зависеть от чтения чего-либо из DOM сразу после изменения состояния. В этих случаях можно использовать ReactDOM.flushSync, чтобы отказаться от пакетной обработки.

Продолжая наш предыдущий пример,

function App() {
  const [count, setCount] = useState(0);
  const [flag, setFlag] = useState(false);
  useEffect(() => {
    console.log({ count, flag });
  }, [count, flag]);
  const handleClick = () => {
    // setCount((c) => c + 1);
    // Force this state update to be synchronous.
    ReactDOM.flushSync(() => setCount(c => c + 1));
    // By this point, DOM is updated.
    setFlag(f => !f);
  };
  return (
    <div className='App'>
      <button onClick={handleClick}>Click Me!</button>
      <h3 style={{ color: flag ? 'blue' : 'black' }}>Count: {count}</h3>
    </div>
  );
}

См. ⚠️ ReactDOM.flushSync: Отказ от автоматической пакетной обработки в обработчиках событий

  • Однако ReactDOM.flushSync встречается нечасто и его следует использовать с осторожностью.

flushSync сбрасывает все дерево и фактически вызывает полный повторный рендеринг для обновлений, которые происходят внутри вызова, поэтому вы должны использовать его очень экономно. Таким образом, гарантия внутренней согласованности между реквизитами, состоянием и ссылками не нарушается.

Чтобы узнать больше об асинхронном поведении этого API и о том, почему setState является асинхронным, ознакомьтесь с замечательным обсуждением Уточнение RFC: почему setState асинхронен? №11527

Автоматическая пакетная обработка в React 18

React 18 включает некоторые стандартные улучшения с ReactDOMClient.createRoot, в том числе поддержку автоматической пакетной обработки.

Начиная с React 18, все обновления будут автоматически пакетироваться, независимо от того, откуда они исходят.

Таким образом, вызов setState внутри обработчиков событий, асинхронных функций, тайм-аутов или любой другой функции будет выполняться автоматически (так же, как и внутри реагирующих событий). Это приведет к меньшему рендерингу и, следовательно, к лучшей производительности в реагирующих приложениях.

function handleClick() {
  fetchSomething().then(() => {
    // React 18 and later DOES batch these:
    setCount(c => c + 1);
    setFlag(f => !f);
    // React will only re-render once at the end (that's batching!)
  });
}

Обсудить в Твиттере

Хотите прочитать это в моем блоге? Оформить заказ в этом блоге