React 18の新機能と並行レンダリング革命

## 概要

本記事ではReactのメジャーバージョンアップであるReact 18で実装された新機能についてご紹介します。このアップデートによりReactはレンダリングを並行化するという大きな革命に挑戦しています。この並行レンダリングという概念を解き明かしながら新機能の使い方やメリットについて解説していきます。

## React 18とは

React 18は2022年3月にリリースされたReactのメジャーバージョンアップです。基本的なアップデート内容については以下の記事に詳しく書かれています。

https://reactjs.org/blog/2022/03/29/react-v18.html

Reactはバージョン16のリリース以来ここ数年大きな機能追加を行いませんでした。一つ前のメジャーバージョンである17では「新機能なし」とされています。

https://reactjs.org/blog/2020/10/20/react-v17.html

その裏では並行モード(Concurrent Mode)と呼ばれる新しい仕組みの開発が進められていました。それが結実したのがReact 18です。新機能は並行モードではなく並行機能(Concurrent Feature)として組み込まれることになりました。新機能を利用した場合のみ処理が変更されるため、アプリケーション全体において破壊的変更をケアする必要なく導入できます[^1]。具体的な導入方法については後述します。

https://github.com/reactwg/react-18/discussions/64

[^1]: > By default, React will render updates synchronously, the same as before in Legacy or Blocking modes. This means that when you first upgrade, before using the new APIs, your app will render the same as before. https://github.com/reactwg/react-18/discussions/64

## 並行レンダリング

React 18で導入された新機能の目的はレンダリングを並行化して高速なユーザー体験を提供することです。

これまでのReactにおいて、一度始まったレンダリングは必ず最後まで行われてから次のレンダリングに移行していました。一度始まったレンダリングを中断することはできず、またそのレンダリングが完了するまで別のレンダリングを始めることはできません。

React 18ではレンダリングの実行中に別のレンダリングを始めたり、レンダリングを途中で停止して破棄することができるようになりました。ただし、最終的なレンダリング結果が状態(state)に正しく対応していることは保証されています。[^2]

[^2]: > With synchronous rendering, once an update starts rendering, nothing can interrupt it until the user can see the result on screen. In a concurrent render, this is not always the case. React may start rendering an update, pause in the middle, then continue later. It may even abandon an in-progress render altogether. React guarantees that the UI will appear consistent even if a render is interrupted. https://reactjs.org/blog/2022/03/29/react-v18.html#what-is-concurrent-react

https://reactjs.org/blog/2022/03/29/react-v18.html#what-is-concurrent-react

## React 18の新機能

React 18の具体的な新機能を見ていきましょう。主要なものは以下の三つです。

- レンダリングのバッチ化
- トランジションの導入
- `<Suspense/>`のサーバサイドレンダリング対応

一つ目の「レンダリングのバッチ化」以外に並行レンダリングが深く関わっています。(「レンダリングのバッチ化」も機能的には独立ですが、非同期なレンダリングによってユーザー体験をよくするという意味で思想的には同じと言えます)

### レンダリングのバッチ化

React 18では複数の状態の更新による再レンダリングがまとめて実行されるようになり、パフォーマンスが向上しています。以下の例では`handleClick()`実行時の再レンダリングは1回しか行われなくなりました。React 17以前では二つの状態の更新に反応して2回レンダリングが行われていました。

```jsx
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);

function handleClick() {
    fetchSomething().then(() => {
        setCount(c => c + 1);
        setFlag(f => !f);
    });
}

return (
    <button onClick={handleClick}>Next</button>
);

// https://github.com/reactwg/react-18/discussions/21 
```

https://github.com/reactwg/react-18/discussions/21

これにより再レンダリングの回数自体が減り、パフォーマンスが向上しています。

### トランジションの導入

React 18では「トランジション」という新機能によって急ぐ必要のない再レンダリングを非同期で処理できるようになりました。例えば`<input/>`要素への入力などユーザーの操作による通常の状態変化は即時レンダリングします。その一方で副作用による検索結果の再レンダリングは裏側で処理して完了次第反映することができます。この機能によって、ユーザーの操作を阻害せずに重いレンダリングを裏側で実行することができるようになりました。

具体的な実装としては以下のように`startTransition()`の引数に渡したコールバック内での状態の更新は「急ぐ必要がない」ものと判断されて裏側で処理されます。処理中に追加で入力されて状態が変化した場合は途中のレンダリングを破棄して、新しい値をもとにレンダリングを始めます。もちろんトランジションで処理されない状態の更新(`setInputValue()`)は即座に反映されます。

```jsx
import {startTransition} from 'react';

setInputValue(input);

startTransition(() => {
  setSearchQuery(input);
});
// https://reactjs.org/blog/2022/03/29/react-v18.html#new-feature-transitions
```

https://reactjs.org/blog/2022/03/29/react-v18.html#new-feature-transitions

以下にReact 18開発者がこの機能の説明のために作成した動画を引用しながら具体的に例示していきます。

まず、以下のようにユーザーがスライダーを動かすことでスケールが変更され、グラフが描画されるアプリケーションがあるとします。グラフの描画は重い処理で、React 17以前で普通に実装するとそのレンダリングを待つことによりスライダーの操作が重くなってしまいます。

![122988019-4658fe00-d36f-11eb-9d86-f930c5f6b201.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/458395/939544b0-ae72-d16c-7c62-714c0ad88f29.gif)

そこでReact 18でトランジションを利用すると、以下のようにユーザーの操作を阻害せずに重いレンダリングを処理することができます。グラフの描画をトランジションによって裏側で処理している間にスライダーの位置変更をレンダリングできるからです。グラフの描画中にさらにスライダーが移動するとそれに応じてグラフが(裏側で)描画され直して、完了次第画面に反映されます。

![122988501-ce3f0800-d36f-11eb-9ff0-e485bc552b45.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/458395/28a1e439-6a3b-4d38-b987-480a03d4e77d.gif)

https://github.com/reactwg/react-18/discussions/65

#### トランジション関係のフック

さらに`useTransition()`フックでレンダリングを後回しにしている状態(`isPending`)と`startTransition()`関数を取得することができます。`isPending`を利用してトランジション中のコンポーネントをローディング表示に切り替えることなどができます。

```jsx
import { useTransition } from 'react';

const [isPending, startTransition] = useTransition();

{isPending && <Spinner />}

// https://github.com/reactwg/react-18/discussions/41
```

https://github.com/reactwg/react-18/discussions/41

`useTransition()`を呼び出すコンポーネント内で更新されない状態の更新に対してにトランジションを適用したい場合は`useDeferredValue()`を使うことができます。以下の例では`query`変数はコンポーネントの外(カスタムフック)で更新されています。`query`の更新をトランジションとして処理したい場合は`useDeferredValue()`に渡します。`query`変数を利用した`SearchInput`要素の更新は即座に行われ、`useDeferredValue()`によって生成された`deferredQuery`変数を利用した`SearchSuggestions`要素の更新はトランジションによって裏側で実行されるようになります。

```jsx
function Typeahead() {
  const query = useSearchQuery('');

  const deferredQuery = useDeferredValue(query);

  return (
    <>
      <SearchInput query={query} />
      <Suspense fallback="Loading results...">
        <SearchSuggestions query={deferredQuery} />
      </Suspense>
    </>
  );
}
// https://github.com/reactwg/react-18/discussions/129
```

https://github.com/reactwg/react-18/discussions/129

### `<Suspense/>`のサーバサイドレンダリング対応

React 17以前での`<Suspense/>`コンポーネントは`React.lazy()`と組み合わせて分割されたコードを遅延読み込みするためのものでした。React 18ではその機能をサーバサイドレンダリング(SSR)でも利用できるようになります。React 18でSSRされたページは`<Suspense/>`で区切られた単位ごとに非同期で生成されて読み込まれます。

例えばページの一部分がサーバ側でデータの取得を行なってからレンダリングする「重い」コンポーネントだとします。そのコンポーネントを`<Suspense/>`で囲んでおくと、その部分のレンダリングを後回しにしつつページの他の部分をブラウザ上で表示することができます。レンダリング中のコンポーネントが表示される場所には既存の`<Suspense/>`と同様にフォールバック(ローディング表示など)を表示しておくことができます。

React 17以前ではサーバサイドレンダリングをこのように分割して行うことができなかったため、一部のコンポーネントのレンダリングに時間がかかる場合ページ全体の表示までの時間に影響していました。React 18で`<Suspense/>`がサーバサイドレンダリングに対応したことで、ページ表示時間の短縮によるユーザー体験の向上が見込まれます。

これを実現する技術は「ストリーミングHTML」と呼ばれています。これまでのSSRで全体をレンダリングし終わった完全なHTMLを配信していました。ストリーミングHTMLではレンダリングが完了したコンポーネントから順次配信していって、重いコンポーネントはレンダリングが完了次第順次置き換えられていきます。[^3]

[^3]: > We are adding a new server renderer that supports streaming HTML out-of-order. Unlike the old server renderer that synchronously produces a string, the new server renderer produces a stream. That stream starts with the initial HTML that can be flushed early. However, the new renderer is also fully integrated with Suspense, which means that it's able to "wait" for parts of the tree that are not ready, and emit fallback HTML (e.g. spinners) for them. When the content is ready, React emits the content HTML in the same stream along with a small inline `<script>` to insert it in the right place in the original DOM structure. As a result, even if some part of the page is slow on the server, the user sees a progressively loading page with all intentionally designed intermediate loading states — even before client JS loads. https://github.com/reactjs/rfcs/blob/main/text/0213-suspense-in-react-18.md#new-feature-server-side-rendering-support-with-streaming

https://github.com/reactjs/rfcs/blob/main/text/0213-suspense-in-react-18.md#new-feature-server-side-rendering-support-with-streaming

#### 選択的ハイドレーション

静的なHTMLを読み込んだ後にページを操作可能にするためにJavaScriptを読み込む処理をハイドレーション(Hydration)と呼びます。React 18ではこのハイドレーションも非同期になりました。

これまではページ全体を操作可能にするJavaScriptを一括で読み込むことしかできませんでした。そうすると常にページ全体のレンダリングが完了するまでユーザーはページを操作できません。React 18ではこのハイドレーションを非同期にすることで、ページ内の「重い」要素のレンダリングを待たずに先に表示された要素を操作可能な状態にできます。[^4]

[^4]: > Selective Hydration lets you start hydrating your app as early as possible, before the rest of the HTML and the JavaScript code are fully downloaded. It also prioritizes hydrating the parts the user is interacting with, creating an illusion of instant hydration. https://github.com/reactwg/react-18/discussions/37

ユーザーが画面を操作することができるようになるまでの待機時間が減少するため、この機能もユーザー体験の改善に寄与すると言えるでしょう。

https://github.com/reactwg/react-18/discussions/37

#### Next.jsでの利用

ただし、このような非同期なSSRはそのためのフレームワークを利用しないと活用しづらい機能です。そこで例えばNext.jsでは`<Suspense/>`を使ったストリーミングSSRをすでに利用できるようになっています。最新版のNext.jsをインストールすれば設定なしで利用できます。[^5]

[^5]: > When you use Suspense in a server-rendered page, there is no extra configuration required to use streaming SSR. https://nextjs.org/docs/advanced-features/react-18/streaming

https://nextjs.org/docs/advanced-features/react-18/streaming

## React 18の利用方法

React 18には破壊的変更が含まれるため、段階的にアップデートできるようになっています。

React 18をインストールしただけでは新機能は利用できません。エントリポイントで使うメソッドを`react-dom`の`render()`から`react-dom/client`の`createRoot()`に変更するとReact 18の新機能が利用できるようになります。

```jsx
// Before
import { render } from 'react-dom';
const container = document.getElementById('app');
render(<App tab="home" />, container);

// After
import { createRoot } from 'react-dom/client';
const container = document.getElementById('app');
const root = createRoot(container);
root.render(<App tab="home" />);

// https://reactjs.org/blog/2022/03/08/react-18-upgrade-guide.html#updates-to-client-rendering-apis
```

https://reactjs.org/blog/2022/03/08/react-18-upgrade-guide.html#updates-to-client-rendering-apis

またトランジションやストリーミングSSRなどの機能を利用した場合のみ並行レンダリングが行われるので、エントリポイントを`createRoot()`に切り替えてもアプリケーション全体の挙動が変わることはありません。

## まとめ

React 18での並行レンダリング機能と、それを利用した新機能についてご紹介しました。それぞれの機能には様々な場面でユーザー体験を向上させるというメリットがあります。またこれらの機能を段階的に導入していくことができる仕組みになっています。

こうした並行レンダリング機能はReactが仮想DOMというアプローチをとっているために可能になっています。

> そして、実は仮想DOMがこの性質に有利に働いています。仮想DOMがある場合、ステート更新により大まかにはコンポーネントの再レンダリング→仮想DOMツリーの構築→実DOMに反映 という動きが起こります。現在のDOM APIでは、実DOMに反映するフェーズが始まってしまうともう中断して割り込むことはできません。

https://qiita.com/uhyo/items/35cb243557df5e1a87fc#%E5%88%86%E6%9E%90

この点からReactはSvelteやSolidなど仮想DOMを利用しないことでユーザー体験を向上させようとしているライブラリとは明確に別の道を選んでいると言えるでしょう。仮想DOMを利用することによるメリットによってユーザー体験を向上させようとしているのです。このアプローチの違いはライブラリ選定の際のポイントになりうるでしょう。

## 参考文献

[React v18.0 – React Blog](https://reactjs.org/blog/2022/03/29/react-v18.html)

[How to Upgrade to React 18 – React Blog](https://reactjs.org/blog/2022/03/08/react-18-upgrade-guide.html)

[React 18: Overview | Next.js](https://nextjs.org/docs/advanced-features/react-18/overview)

[Introducing React 18 · Discussion #4 · reactwg/react-18](https://github.com/reactwg/react-18/discussions/4)

[Behavioral changes to Suspense in React 18 · Discussion #7 · reactwg/react-18](https://github.com/reactwg/react-18/discussions/7)

[New Suspense SSR Architecture in React 18 · Discussion #37 · reactwg/react-18](https://github.com/reactwg/react-18/discussions/37)

[What happened to concurrent "mode"? · Discussion #64 · reactwg/react-18](https://github.com/reactwg/react-18/discussions/64)

[New in 18: useDeferredValue · Discussion #129 · reactwg/react-18](https://github.com/reactwg/react-18/discussions/129)

[WEB+DB PRESS Vol.129｜技術評論社](https://gihyo.jp/magazine/wdpress/archive/2022/vol129)

[Reactに有利なベンチマークを作ってみた - Qiita](https://qiita.com/uhyo/items/35cb243557df5e1a87fc)