React 18で導入された新たなフック
フックとは、クラス形式のコンポーネントで利用していたstateなどの機能を、関数形式のコンポーネントでも利用できるようにする機能です。前回記事では、並行レンダーに関連してReact 18で導入されたuseTransition、useDefferedValueフックを説明しました。今回はそれ以外にReact 18で導入されたフックを紹介します。
一意のIDを生成できるuseIdフック
useIdフックは、一意のIDを生成するフックです。図6のサンプルでは、2つのコンポーネントでそれぞれIDを生成し、それを利用して入力フォームを表示します。
図6のサンプルでは、Appコンポーネント内でCheckboxSample1、CheckboxSample2の2つのコンポーネントを表示します(リスト3)。
function App() { return ( <div> <CheckboxSample1/> <CheckboxSample2/> </div> ); } export default App;
CheckboxSample1とCheckboxSample2の実装内容はほとんど同一です。CheckboxSample1コンポーネントの実装をリスト4に示します。
function CheckboxSample1() { // 一意となるIDを生成 ...(1) const id = useId(); return ( <div> <h3>React 18 useId Checkbox1</h3> <div>このコンポーネント用のIDは「{id}」</div> <div> <input id={id + '-check1'} type="checkbox"></input> <label htmlFor={id + '-check1'}>Samsung</label> </div> <div> <input id={id + '-check2'} type="checkbox"></input> <label htmlFor={id + '-check2'}>Sony</label> </div> <div> <input id={id + '-check3'} type="checkbox"></input> <label htmlFor={id + '-check3'}>SHARP</label> </div> </div> ); } export default CheckboxSample1;
(1)のuseIdで一意のIDを生成して変数idに格納し、コンポーネントの表示内容を指定するreturnメソッド内で利用しています。CheckboxSample1とCheckboxSample2の各コンポーネントでuseIdを実行すると、異なるIDが取得できることが保証されるため、<input>や<label>タグに指定するIDの衝突を避けることができます。
DOM更新前に実行されるuseInsertionEffectフック
useInsertionEffectフックは、コンポーネントのDOM構造が更新される前に実行される副作用を記述できるフックです。従来から利用できた副作用フックであるuseEffect、useLayoutEffectと併せて、図7のサンプルで説明します。このサンプルでは、useInsertionEffectでCSS(画面の背景色)を追加し、useLayoutEffectとuseEffectでそれぞれコンポーネント内の表示を更新します。
このコンポーネントには、表示される多数の文字列が含まれるため、表示に時間がかかります。この場合、useEffectで更新する表示は、更新前の表示が一瞬見えますが、useLayoutEffectで更新する表示は更新前の表示が見えません(図8)。
このサンプルのAppコンポーネントを、リスト5に示します。
function App() { // 画面に表示するメッセージのstate ...(1) const [msg1, setMsg1] = useState('This is default msg1.'); const [msg2, setMsg2] = useState('This is default msg2.'); const msg3 = 'This is default msg3.'; // DOM更新前に(useLayoutEffect、useEffectより先に)動く ...(2) useInsertionEffect(() => { // CSSを設定できる ...(2a) const css = document.createElement('style'); css.textContent = 'body {background-color:lightblue;}'; document.head.appendChild(css); }, []); // DOM更新後に、同期的に動く(処理完了まで画面が更新されない) ...(3) useLayoutEffect(() => { // もとのmsgは見えずに、このメッセージが見える ...(3a) setMsg1('Hello from useLayoutEffect!'); }, []); // DOM更新後に、非同期に動く(処理完了前に画面が更新される) ...(4) useEffect(() => { // もとのmsgが一瞬見えた後、このメッセージが見える ...(4a) setMsg2('Hello from useEffect!'); }, []); return ( (略) ); } export default App;
(1)で画面に表示するメッセージのstateを定義しています。msg3は変更されることがないので固定値とします。(2)のuseInsertionEffectフックは、DOM更新前に動作します。ここでは(2a)の処理で、Webページの背景色を設定するCSSを設定しています。
useLayoutEffect(3)とuseEffectフック(4)は、それぞれDOM更新後に動作しますが、前者は同期的に、後者は非同期で動くという違いがあります。ここではそれぞれ(3a)と(4a)で、メッセージのstate(msg1、msg2)を更新していますが、useLayoutEffectではメッセージ更新前の状態が画面に見えず、useEffectでは一瞬見えるという違いがあります。
まとめると、useInsertionEffectはDOM更新前に実行できる副作用(CSSの設定など)、useLayoutEffectは画面の表示内容を更新する副作用、useEffectはそれ以外の副作用で利用することが望ましいといえます。
なお、サンプルコードには、表示を遅くするために多数の文字列を画面に表示する処理が含まれています。詳細はサンプルコードを参照してください。
[補足]useInsertionEffectはライブラリー作者が便利に使える
Reactの公式ドキュメントにおいて、useInsertionEffectは、JavaScriptでCSSを設定する(CSS-in-JS)ライブラリーの作者のためのものであり、一般にはuseEffectかuseLayoutEffectを利用するよう案内されています。
[補足]useLayoutEffectではレンダリングのブロックに注意
上述の通り、useLayoutEffectはDOM更新後に「同期的に」動作するため、画面の表示内容を更新する処理を実装すれば、更新前の画面を見せずに画面を更新できます。
一方で、useLayoutEffectで重い処理を行うと、同期的な動作のためレンダリングがブロックされ、パフォーマンスに影響を与えます。その意味で、通常の副作用はuseEffectで実行し、useLayoutEffectは画面の表示内容に関連した重くない処理に限定して利用するべきといえます。
外部ストアからデータを画面に反映できるuseSyncExternalStoreフック
useSyncExternalStoreフックは、Reactの外部で管理されているストア(データの供給源)からデータを画面に反映できるフックです。本記事ではJavaScriptで取得できる日時を外部ストアとみなして、useSyncExternalStoreフックで画面に現在日時を表示する図9のサンプルで利用方法を説明します。
このサンプルのApp.jsをリスト6に示します。
function App() { // 外部ストアの現在の値を取得する処理 ...(1) const getSnapshot = () => { return new Date().toString(); // 現在の日時を返却 ...(1a) }; // 外部ストアが変更されるたびに呼び出されるcallbackを登録・登録解除する処理 ...(2) const subscribe = (callback) => { // callback登録:setIntervalで1秒おきにcallbackが呼ばれるようにする ...(2a) const timer = setInterval(callback, 1000); // callback登録解除:clearIntervalでcallbackの実行を停止 ...(2b) return () => clearInterval(timer); }; // 外部ストアの値(現在の日時)を参照できる変数snapshotを取得 ...(3) const snapshot = useSyncExternalStore(subscribe, getSnapshot); return ( <div> <h3>React 18 useSyncExternalStore</h3> { snapshot } {/* (4) */} </div> ); } export default App;
useSyncExternalStoreフック(3)の引数には、外部ストアの現在の値を取得する処理(1)と、外部ストアが変更されるたびに呼び出されるcallbackの登録・登録解除処理(2)を指定します。ここでは(1a)で現在の日時を取得し、(2a)のsetIntervalと(2b)のclearIntervalで、定期的に実行するcallbackを登録・登録解除します。これらを利用して(3)で取得したsnapshotを、(4)の通り画面に表示するよう記述すると、外部ストアの現在値(=ここでは現在日時)が表示され、callbackの処理により1秒おきに更新されるようになります。
[補足]useSyncExternalStoreはReactでない既存のコードとの接続用
Reactの公式ドキュメントにおいて、useSyncExternalStoreは、Reactではない環境で記述された既存のコードからデータを取得する際に有効であり、React内のデータを取得するにはuseStateやuseReducerを利用するように案内されています。
[補足]useEffectとuseSyncExternalStoreの使い分け
外部データをReactのコンポーネントに反映させるには、useEffectフックで外部からデータを取得して反映する方法もあります。この場合、useEffectフックはレンダリング後にしか実行されないため、外部データの更新を反映するには、まずレンダリングが行われる必要があります。
一方でuseSyncExternalStoreを利用すると、データが更新されたことをcallbackでReactコンポーネントに通知して再レンダリングさせることができます。そのためReactコンポーネントと独立して更新される外部データを参照する場合、useSyncExternalStoreが有用です。
まとめ
本記事では、前回に引き続きReact 18の新機能について説明しました。今回は潜在的な問題点を洗い出すStrictモードへの機能追加と、React 18で導入された新たなフックとしてuseId、useInsertionEffect、useSyncExternalStoreについて紹介しました。
前回説明したTranstionやSuspenseなどを含め、React 18は、開発者にとってはよりシンプルで本質的な実装、利用者にとってはWebページの使い勝手向上が期待できるアップデートといえます。