🔍

React 18. createRoot

React 18. createRoot

React 18 включает два корневых API, которые называются Legacy Root API и New Root API.

  • Legacy Root API (Устаревший корневой API): это существующий API, вызываемый с помощью ReactDOM.render. Этот метод создает корень приложения, работающий в «устаревшем» режиме, который работает точно так же, как в React 17. Перед релизом будет добавлено предупреждение, указывающее, что метод устарел, и, что необходимо переключиться на New Root API.

  • New Root API (Новый корневой API): новый корневой API вызывается с помощью ReactDOM.createRoot. Это создает корень приложения, работающий в React 18, который добавляет все улучшения React 18 и позволяет использовать параллельные функции. Это будет основной корневой API в дальнейшем.

Что такое корень приложения?

В React «корень» - это указатель на структуру данных верхнего уровня, которую React использует для отслеживания дерева при рендеринге. В устаревшем API корень был непрозрачен для пользователя, потому что он был прикреплён к элементу DOM и доступ к нему был через узел DOM:

import * as ReactDOM from 'react-dom';
import App from 'App';

const container = document.getElementById('app');

// Initial render.
ReactDOM.render(<App tab="home" />, container);

// During an update, React would access
// the root of the DOM element.
ReactDOM.render(<App tab="profile" />, container);

В New Root API сначала создаётся корень приложения, а затем вызывается его рендеринг:

import * as ReactDOM from 'react-dom';
import App from 'App';

const container = document.getElementById('app');

// Create a root.
const root = ReactDOM.createRoot(container);

// Initial render: Render an element to the root.
root.render(<App tab="home" />);

// During an update, there's no need to pass the container again.
root.render(<App tab="profile" />);

В чём разница?

API бы изменён по нескольким причинам.

Во-первых, это исправляет некоторую эргономику API при запуске обновлений. Как показано выше, в устаревшем API вам нужно продолжать передавать контейнер в рендер, даже если он никогда не изменится. Это также означает, что нам не нужно хранить корень на узле DOM, хотя мы все еще делаем это сегодня.

Во-вторых, это изменение позволяет нам удалить метод гидратации (hydrate) и заменить его методом корня; и удалите обратный вызов рендеринга, что не имеет смысла в мире с частичной гидратацией.

А как насчет гидратации?

Функция hydrate была перенесена в API hydrateRoot.

До:

import * as ReactDOM from 'react-dom';
import App from 'App';

const container = document.getElementById('app');

// Render with hydration.
ReactDOM.hydrate(<App tab="home" />, container);

После:

import * as ReactDOM from 'react-dom';

import App from 'App';

const container = document.getElementById('app');

// Create *and* render a root with hydration.
const root = ReactDOM.hydrateRoot(container, <App tab="home" />);
// Unlike with createRoot, you don't need a separate root.render() call here

Обратите внимание, что в отличие от createRoot, hydrateRoot принимает обычный JSX в качестве второго аргумента. Это связано с тем, что первоначальный рендер клиента является особенным и должен соответствовать дереву на сервере.

Если вы хотите снова обновить корень после гидратации, вы можете сохранить его в переменной, как и в случае с createRoot, и вызвать root.render():

import * as ReactDOM from 'react-dom';
import App from 'App';

const container = document.getElementById('app');

// Create *and* render a root with hydration.
const root = ReactDOM.hydrateRoot(container, <App tab="home" />);

// You can later update it.
root.render(<App tab="profile" />);

А как насчет обратного вызова рендеринга?

В устаревшем корневом API вы можете передать обратный вызов render, который вызывается после рендеринга или обновления компонента:

import * as ReactDOM from 'react-dom';
import App from 'App';

const container = document.getElementById('app');

ReactDOM.render(container, <App tab="home" />, function() {
  // Called after inital render or any update.
  console.log('rendered');
});

В New Root API был удалён этот обратный вызов.

При частичной гидратации и прогрессивном SSR время для этого обратного вызова не будет соответствовать ожиданиям пользователя. Чтобы избежать путаницы, рекомендуется использовать requestIdleCallback, setTimeout или обратный вызов ref для корня.

Итак, вместо этого:

import * as ReactDOM from 'react-dom';

function App() {
  return (
    <div>
      <h1>Hello World</h1>
    </div>
  );
}

const rootElement = document.getElementById("root");

ReactDOM.render(<App />, rootElement, () => console.log("renderered"));

Вы можете сделать это:

import * as ReactDOM from 'react-dom';

function App({ callback }) {
  // Callback will be called when the div is first created.
  return (
    <div ref={callback}>
      <h1>Hello World</h1>
    </div>
  );
}

const rootElement = document.getElementById("root");

const root = ReactDOM.createRoot(rootElement);
root.render(<App callback={() => console.log("renderered")} />);

Посмотреть в codesandbox

Зачем оставлены оба метода?

Это сделано по двум причинам:

  • Плавное обновление: разработчики React хотели, чтобы пользователи при обновлении до 18-ой версии избежали сломанного приложения, а видели предупреждение в консоли для устаревшего корневого API, в котором рекомендуется использовать новый API.

  • Экспериментирование: некоторые приложения могут выбрать запуск продакшн эксперимента для сравнения устаревшего корня с новым корнем, который включает улучшения производительности "из коробки". Включив оба варианта, будет проще проводить эксперименты.

Читать ещё:

React 18. startTransition

React 18. startTransition

Стилизация select на чистом CSS

Стилизация select на чистом CSS