はじめに
Reactは、動的なWebページのインターフェースを構築するためのJavaScriptライブラリーで、Facebookとオープンソースコミュニティによって開発が続けられています。
Reactは2022年3月にメジャーアップデートであるバージョン18(以下「React 18」がリリースされ、現在の最新バージョンは18.2.0です。React 18の概要は公式ブログ記事で紹介されています。
本記事では、React 18で導入された改良や新機能について、今回と次回の2回にわたって、サンプルを交えながら紹介していきます。
対象読者
- 新バージョンでReactを体験してみたい方
- すでにReact開発に従事していて、新機能を知りたい方
- Reactの最新動向を追っておきたい方
必要な環境
本記事のサンプルコードは、以下の環境で動作を確認しています。
- Windows 10 64bit版
- React 18.2.0
- Node.js v18.16.0
- Microsoft Edge 114.0.1823.43
サンプルコードを実行するには、サンプルのフォルダーで「npm install」コマンドを実行してライブラリーをダウンロード後、「npm run start」コマンドを実行して、「https://localhost:3000」をWebブラウザーで表示します。本記事内ではReact 18の新機能に関する本質的な部分を抽出して説明しますので、それ以外の実装は各サンプルコードを参照してください。
React 18新機能のバックグラウンドとなる並行レンダー
最初に、React 18の新機能のバックグラウンドである「並行レンダー」を紹介します。従来、Reactにおいては、画面を更新するレンダー処理が始まったら、レンダーが完了してユーザーに画面が表示されるまで、画面の更新が中断されることはありませんでした。
しかし、React 18の並行レンダーでは、画面の更新処理が途中で中断、再開されることや、途中までの処理が打ち切られてしまうこともあります。このように、画面の更新時に並行処理を許すことで、後述する画面更新の優先順位指定など、利用者の使い勝手を向上するさまざまな新機能が提供されるようになりました。
React 18の新機能を有効にする初期化処理の方法
React 18を利用したサンプルを紹介する前に、ReactのプロジェクトでReact 18の機能を有効にする初期化方法を紹介します。
従来Reactを初期化するには、ReactDOM.renderメソッドにルートコンポーネントとそれを表示するHTML要素を指定して実行していました。
import ReactDOM from 'react-dom'; ReactDOM.render(<App />, document.getElementById('root'));
React 18では、ReactDOM.createRootメソッドに表示先のHTML要素を指定して実行し、その戻り値にルートコンポーネントを渡してrenderメソッドを実行します。
import ReactDOM from 'react-dom/client'; const root = ReactDOM.createRoot(document.getElementById('root')); root.render(<App />);
React 18の環境でも従来の記述(リスト1)で初期化することは可能ですが、その場合、React 18の新機能は有効にならず、React 17相当の挙動になります。
複数の状態変更をまとめて画面に反映するバッチングの範囲拡大
Reactにおいては従来より、ボタンクリックなどReactのイベントハンドラー内部で発生した複数の状態変更を、まとめて1回で画面に反映する「バッチング」が行われていました。
React 18ではさらに、PromiseやsetTimeout、JavaScriptネイティブのイベントハンドラーでもバッチングが行われるようになり、不要な再レンダリングが減少しました。この挙動を図3のサンプルで説明します。2個のボタンのどちらを押しても、画面上の数字と、その色が変更されます。
図3のコンポーネント(App.js)は、リスト3の通りになっています。
function App() { // state ...(1) const [count, setCount] = useState(0); const [flag, setFlag] = useState(false); // 直接stateを更新する処理 ...(2) function onClickButton1() { console.log('=== 直接stateを更新 ==='); setCount(c => c + 1); setFlag(f => !f); } // Promise経由でstateを更新する処理 ...(3) function onClickButton2() { console.log('=== Promise経由でstateを更新 ==='); fetchSomething().then(() => { setCount(c => c + 1); setFlag(f => !f); }); } // 100ms後に成功するPromiseを取得する処理 ...(4) function fetchSomething() { return new Promise((resolve) => setTimeout(resolve, 100)); } (略) }
(1)でuseStateフックを利用して、数字countと、Boolean値flagのstateを定義します。(2)はボタンクリック時のイベントハンドラーで、setCount、setFlagメソッドで状態を変更します(countを1増加、flagのtrue/falseを反転)。一方(3)では、fetchSomethingメソッド(4)で得られるPromiseにより、ボタンクリック後100ms後に(2)と同じ状態変更を行います。
Appコンポーネントの画面内容はリスト4の通り記述します。(1)により、画面のレンダリングが行われるたびに、console.logでログが出力されます。
// 画面内容を返却 return ( <div> <h3>React 18 自動バッチング</h3> <button onClick={onClickButton1}>直接stateを更新</button> <button onClick={onClickButton2}>Promise経由でstateを更新</button> <div>↓ボタン押下ごとに数字が増えて色が変わります</div> <div style={{ color: flag ? 'blue' : 'red', fontSize: '25pt' }}>{count}</div> {console.log('レンダリング実行')} {/* (1) */} </div> );
Reactの起動処理を従来の記述(リスト1)にして図3のサンプルを実行すると、countとflagの2つの状態変更が、Reactのイベントハンドラーでは1回のレンダリングで処理されますが、Promiseでの実行では2回行われることがわかります(図4)。
React 18から導入された新しい起動方法(リスト2)で起動した場合、図4の通り、Reactのイベントハンドラー、Promiseのいずれにおいても、countとflagの2つの状態変更が、1回のレンダリングで実行されます(図5)。