
Можно подумать, что хук 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!)
});
}
- Обратите внимание, что это автоматическое группирование будет работать только в React 18 с
ReactDOM.createRoot. - React 18 с устаревшим
ReactDOM.renderсохраняет старое поведение - Чтобы узнать больше об автоматической пакетной обработке в React 18, см. Автоматическая пакетная обработка для меньшего количества рендеров в React 18 #21.
Хотите прочитать это в моем блоге? Оформить заказ в этом блоге