React использует пакетную обработку для группировки обновлений состояния в обработчиках событий и встроенных хуках. Это предотвращает повторное отображение компонентов при каждом обновлении состояния и повышает производительность приложения.

React 17 и предыдущие версии поддерживают пакетную обработку только для событий браузера. В обновленном React 18 представлен улучшенный способ пакетной обработки под названием Automatic Batching. Он позволяет выполнять автоматическое пакетирование для всех обновлений состояния, независимо от места их поступления.

Пакетная обработка в React 17

Как уже упоминалось, React 17 поддерживал пакетную обработку только событий браузера и хуков. Поэтому обновления состояния происходили только при асинхронных операциях. Следовательно, собственные обработчики событий не были пакетированы.

Для наглядности рассмотрим пример обновления состояния в компоненте React:

import { useState } from "react";
import "./App.css";const App = () => { const [additionCount, setAdditionCount] = useState(0);
const [subtractionCount, setSubtractionCount] = useState(0);

console.log("Component Rendering");

const handleOnClick = () => {
setAdditionCount(additionCount + 1);
setSubtractionCount(subtractionCount - 1);
};

return (
<div>
<button style = {{ width: "50%", height: "30%" }}
onClick = {()=>{
handleOnClick();
}}
>
Click Me!
</button><div>
Add Count: {additionCount}
</div>
<div>
Substraction Count: {substractionCount}
</div></div>
);
};
export default App;

В приведенном примере функция handleOnClick() будет вызываться, когда пользователь нажмет на кнопку. Она будет выполнять обновление двух состояний при каждом нажатии.

Как показано ниже, на консоли браузера сообщение Component Rendering (“Отрисовка компонента”) регистрируется только один раз для обоих обновлений состояния.

Таким образом, React выполняет пакетную обработку обоих обновлений состояния и выполняет повторный рендеринг компонента только один раз.

Но что, если выполнение обновлений состояния не будут связаны с браузером?

Рассмотрим для примера вызов fetch(), который асинхронно загружает данные:

// обработчик событий
const handleOnClickAsync = () => {
fetch(“https://jsonplaceholder.typicode.com/todos/1").then(() => {
setAdditionCount(additionCount + 1);
setSubstractionCount(substractionCount — 1);
});
};

// html
<button style={{ width: “50%”, height: “30%” }}
onClick ={() => {
handleOnClickAsync();
}}
>
Click Me Async Call!
</button>

В приведенном выше примере обновление состояния происходит при обратном вызове функции fetch(). После ее выполнения на консоли браузера появится 2 сообщения. Это указывает на то, что для каждого обновления состояния происходит два отдельных повторных рендеринга.

Недостатки описанного этого процесса

Для небольших приложений процесс повторного рендеринга не критичен. Но по мере их роста количество вложенных компонентов будет увеличиваться. А значит, при каждом обновлении состояния в родительском компоненте  —  без группирования обновлений  —  будет происходить повторная отрисовка всего дерева компонентов.

Такое поведение может вызвать серьезные проблемы с производительностью, связанные со скоростью работы приложения. Именно поэтому в React 18 была внедрена автоматическая пакетная обработка.

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

React v18 гарантирует, что обновления состояния, вызываемые из любого места, по умолчанию будут пакетированными. При этом будут пакетно обрабатываться обновления состояния, имеющие отношение к собственным обработчикам событий, асинхронным операциям, тайм-аутам и интервалам.

Рассмотрим на примере простейшего приложения React, как работает автоматическое пакетирование.

Для начала создадим React-проект последней бета-версии React 18 (npm install react@beta react-dom@beta).

После этого обновим файл index.js, чтобы использовать API createRoot() с функционалом React 18, включающим автоматическое пакетирование:

import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";

const container = document.getElementById("root");
// Создаем корень.
const root = ReactDOM.createRoot(container);
// Рендерим верхний компонент в корень.
root.render(<App />);
reportWebVitals();

Теперь обновим файл App.js с 3 слушателями событий, где 1 слушатель событий содержит обновления состояния, вызываемые:

  • обработчиками событий;
  • асинхронными операциями;
  • тайм-аутами.
import logo from "./logo.svg";
import "./App.css";
import { useState } from "react";

const App = () => {

const [count, setCount] = useState(0);
const [clicked, setClicked] = useState(false);

console.log("React 18 Application Re-Rendering");

// Событие нажатия на кнопку
const handleClick = () => {
  // 1 повторный рендеринг
  setClicked(!clicked);setCount(count + 1); 
};
  
// асинхронная операция
const handleAsyncClick = () => {      
  
fetch("https://jsonplaceholder.typicode.com/todos/1").then(() => {
    // trigger 1 re-render due to React 18 Improved Batching
    setClicked(!clicked);
    setCount(count + 1);
  });
};
  
// тайм-аут/интервал
const handleTimeOutClick = () => {
  setTimeout(() => {
     // запуск 1 повторного рендеринга благодаря функции оптимизированного пакетирования React 18
     setClicked(!clicked);
    setCount(count + 1); 
   });
 };
    
 return (
   <div className="App">
     <header className="App-header">
       <div> Count: {count} </div>
       <div> Clicked: {clicked} </div>
       <button onClick={handleClick}> Event Handler </button>
       <button onClick={handleAsyncClick}> Async Handler </button>
       <button onClick={handleTimeOutClick}> Timeout Handler </button>
     </header>
   </div>
 );
};
    
export default App;

Нажатие на три кнопки выводит на консоль браузера три лога. При этом в каждом обработчике событий произойдет два обновления состояния.

Приведенный выше пример показывает оптимальный процесс повторного рендеринга, где каждое событие вызывает только одну повторную отрисовку, поскольку React пикетирует все обновления состояния независимо от места вызова.

Как видите, React 18 улучшил способ обработки повторных отрисовок компонентов. В React 17 был пробел, из за которого события, не связанные с браузером, не подлежали пакетной обработке. React 18 восполнил этот пробел, повысив производительность приложения за счет устранения ненужных повторных рендеров.

Как остановить автоматическое пакетирование

Автоматическая пакетная обработка  —  замечательная функция. Но могут возникнуть ситуации, когда понадобится предотвратить ее действие. Для этого React предлагает метод flushSync() в react-dom. Этот метод позволяет вызвать повторный рендеринг для определенного обновления состояния.

Как работает Flush Sync

Чтобы использовать этот метод, нужно импортировать его из react-dom, используя синтаксис:

import { flushSync } from 'react-dom';

Затем следует вызвать метод в обработчике событий и поместить обновление состояния в тело flushSync().

const handleClick = () => {
flushSync(() => {
setClicked(!clicked);
// react создаст здесь повторный рендеринг
});

setCount(count + 1);
// react создаст повторный рендеринг здесь
};

Когда событие будет вызвано, React обновит DOM один раз в flushSync(), а затем снова обновит DOM в setCount(count + 1), избегая пакетной обработки.

Заключение

Автоматическое пакетирование  —  одна из самых полезных функций релиза React 18. Мы подробно рассмотрели проблемы с не совсем корректным процессом пакетной обработки и то, как автоматическая пакетная обработка их решает.

Полный пример приведенного выше кода можете найти в репозитории GitHub.

Читайте также:

Читайте нас в TelegramVK и Яндекс.Дзен


Перевод статьи Lakindu Hewawasam: Automatic Batching in React 18: What You Should Know

Предыдущая статьяЯзык C: константы и литералы
Следующая статьяЛенивая загрузка, агрегирование и CQRS