React公式ドキュメントサイトreact.devが2023年3月16日に改訂されました(Blog「Introducing react.dev」参照)。基本解説の「Learn React」には、初心者から読み進められる丁寧な説明から中級者にも役立つ詳しい情報まで、内容は満載です。
それらの記事をTypeScriptの備わった環境で解説し直し、Qiitaに投稿してきました。数がある程度増えたので、拙著Qiita記事と公式サイトの対応リンクをまとめ、簡単な紹介を添えたのが本稿です。各解説は基本的に公式記事の情報はカバーしているものの、邦訳ではありません。足りない部分は補ったり、説明の仕方を改めたり、不要と思われる記述は削除しました。また、TypeScriptを前提に加えたので、JavaScriptの基礎的な説明は省いています。
なお、コード例の一部はCodeSandboxに公開しているので、興味があれば実際にお試しください。
[更新2023/08/16]「カスタムフックでロジックを再利用する」を追加。
Describing the UI
ユーザーインタフェースを組み立てる
主題はReactのコンポーネントと戻り値JSXの書き方です。Reactアプリケーションはコンポーネントを組み合わせて、JSXでページを描画します。
Adding Interactivity
発生したイベントを処理する
解説するのは、つぎのような項目です。
- イベントハンドラをどう定め方るか。
- イベント処理のロジックを親コンポーネントから子にどのように渡すか。
- イベントの制御の仕方。
Reactのイベントハンドラは、JSXにプロパティとして加え、ハンドラ(コールバック)関数を与えます。要素へのマウスクリックやポインタの重なり、フォーム入力のフォーカスといったインタラクションなどに反応して呼び出される関数です。
コンポーネントの状態を保持する
コンポーネントに対するインタラクションに応じて、画面の中身を改めなければならないことがあるでしょう。たとえば、つぎのような場合です。
- フォームに入力したときテキストフィールドの更新。
- カルーセルで[つぎへ]のボタンをクリックしたときの画像の切り替え。
- 商品の[購入]ボタンクリックによるショッピングカートへの追加。
コンポーネントは、こうした変更の現行値を覚えておかなければなりません。Reactでは、コンポーネントごとに値をメモリする仕組みは状態(state)と呼ばれます。
レンダーとコミット
コンポーネントは画面に表示される前に、Reactがレンダリングしなければなりません。コンポーネントがレストランの料理人だとしましょう。すると、Reactは受けた注文にもとづき、客に料理を運ぶウェイターの役割です。
- レンダーを起動: 客の注文を厨房に届けます。
- コンポーネントをレンダリング: 厨房で料理をつくりあげる仕事です。
- DOMにコミット: 料理を客のテーブルに運びます。
スナップショットとしての状態
状態変数は、標準JavaScriptの変数のように、値が読み書きできると思えるかもしれません。けれど、状態のふるまいは「スナップショット」(筆者注: そのときどきの情報の記録)に近いです。状態の設定はすでに定められた値の変更ではありません。再レンダーの起動です。
連続した状態変更をキューに加えて処理する
状態変数を設定すると、レンダリングはキューに入ります。けれど、つぎのレンダリングがキューに加えられる前に、変数値に複数の操作を行いたい場合もあるでしょう。そのためには、Reactが状態の更新をどうバッチ処理するのかを理解しておくと役立ちます。
状態に収めたオブジェクトを更新する
Reactの状態に保持できるのは、オブジェクトを含めたすべてのJavaScriptの値です。ただし、状態に持たせたオブジェクトを直接変更してはいけません。更新したいときは、まず新たなオブジェクト(またはもとオブジェクトの複製)をつくって編集してください。そのうえで、状態はそのオブジェクトを用いて設定するのです。
状態に収めた配列を更新する
配列はJavaScriptではミュータブルです。けれど、Reactで状態に収める場合は、イミュータブルとして扱ってください。配列もオブジェクトです。したがって、状態に入れた配列の更新には、まず新たな配列(またはもと配列の複製)をつくります。そのうえで、編集を加えて状態に設定するのです。
Managing State
入力を状態に反応させる
Reactが用いるUIの操作方法は宣言的です。それぞれのUI部分は直接いじりません。コンポーネントが取り得るさまざまな状態を記述し、ユーザー入力に応じて切り替えるのです。デザイナーがUIを考えるのと似ているかもしれません。
状態の構造を選択する
コンポーネントの変更やデバッグがしやすいか、逆にバグに悩まされるかという差につながるのが、状態を適切に構造化するかどうかです。状態を構造化するときに考えるべき点についてご紹介します。
コンポーネント間で状態を共有する
ふたつのコンポーネントの状態を、つねに一緒に変化させたい場合があるでしょう。そのためには、状態をそれぞれのコンポーネントにもたせるのでなく、もっとも近い共通の親に移すべきです。そのうえで、親から子には値をプロパティで渡します。これは状態の引き上げ(リフトアップ)と呼ばれ、Reactのコードでよく用いられるやり方です。
状態の保持とリセット
状態はコンポーネントごとに別々です。Reactはどの状態がどのコンポーネントに属するか、UIツリー内の位置にもとづいて把握します。そして、再レンダリング間で、状態をいつ保持し、いつリセットするかは制御できるのです。
状態のロジックをリデューサに切り出す
様々な状態更新をもつコンポーネントが多くのイベントハンドラにまたがると、手に負えなくなりそうです。こういう場合、すべての状態更新のロジックをコンポーネントから切り出して、ひとつの関数にまとめることが考えられます。これがリデューサです。
コンテクストでデータを深い階層に渡す
通常、親コンポーネントから子には、プロパティで情報を渡します。けれど、プロパティを用いることが、冗長で不便になるのがつぎのような場合です。
- 間に多くのコンポーネントを経て渡すことになってしまう(いわゆる「バケツリレー」)。
- アプリケーション内の多くのコンポーネントが同じ情報を必要としている。
コンテクストを使えば、親コンポーネントの情報が、ツリーの下層のどのコンポーネントからでも参照できるようになります。ツリーの深さは問わず、プロパティを介する必要もありません。
リデューサとコンテクストで拡張性を高める
コンポーネントの状態更新のロジックを統合するのがリデューサです。また、コンテクストを用いれば、情報を階層深くの他のコンポーネントに渡せます。このふたつを組み合わせることにより、複雑な画面の状態も管理しやすくなるでしょう。
Escape Hatches
refで値を参照する
コンポーネントに何か情報を「記憶」させつつ、その情報による新たなレンダーは引き起こしたくないとき使えるのがref
です。
refでDOMを操作する
ReactがDOMを更新してレンダリング出力に合わせる処理は自動的です。そのため、コンポーネントがDOMを操作しなければならないことはあまりありません。けれど、DOMを参照してReactに操作させたい場合も出てくるでしょう。たとえば、つぎのようなときです。
- ノードにフォーカスしたい。
- ノードをスクロールさせたい。
- ノードのサイズや位置を調べたい。
もっとも、Reactにはそのような組み込みの仕組みはありません。そこで、refによりDOMノードを参照するのです。
React + TypeScript: エフェクトを使って同期する
コンポーネントには、外部システムと同期しなければならないものもあります。たとえば、つぎのような場合です。
- React以外のコンポーネントを状態にもとづいて制御したいとき。
- サーバーとの接続を確立したいとき。
- 分析用のログをコンポーネントが画面に表示されたら送りたいとき。
エフェクトは、レンダリング後にそのコードを実行します。すると、Reactの外のシステムとコンポーネントを同期できるのです。
エフェクト(useEffect)を使わなくてよい場合とは
エフェクトは、Reactの仕組みからの非常口です。Reactの「外に出て」コンポーネントを外部システムと同期できます。たとえば、React以外のウィジェットやネットワーク、あるいはブラウザDOMなどです。外部システムが関わらないなら、エフェクトは要りません。コンポーネントの状態を、プロパティや状態が変わったら更新したいといった場合です。不要なエフェクトを除けば、コードはわかりやすく、実行も速くなり、エラーが起こりにくくなります。
リアクティブなエフェクト(useEffect)のライフサイクル
エフェクトとコンポーネントとは異なるライフサイクルです。
- コンポーネント
- マウント
- 更新
- アンマウント
- エフェクト
- 同期の開始
- 同期の停止
エフェクトの依存するプロパティや状態が時間とともに変化する場合、ライフサイクルは繰り返されるかもしれません。Reactの提供するリンタールールにより、エフェクトに正しく依存が定められているか確かめられます。こうして、エフェクトは最新のプロパティや状態と同期されるのです。
イベントとエフェクトにロジックを分ける
イベントハンドラは、あらかじめ定めたインタラクションを行ったときにのみ再実行されます。それに対して、プロパティや状態変数のような読み取った値が、直前のレンダリング時と異なるとき再同期するのがエフェクトです。場合によっては、ふたつの動きを組み合わせたいかもしれません。エフェクトをある値に応じては再実行し、他の値には応答させないというときです。こういう処理の考え方についてご説明しましょう。
エフェクトの依存を除く
エフェクトを書くと、リンターはコードに(プロパティや状態などの)リアクティブな値が含まれているかどうか確かめます。それらの値は、すべてエフェクトが読み取る依存配列に加えられていなければなりません。これにより、エフェクトはコンポーネントの最新のプロパティや状態と同期が保てるのです。要らない依存値が含まれていると、エフェクトは必要以上に実行されたり、無限ループを引き起こすかもしれません。本稿が解説するのは、エフェクトの不要な依存値をいかに見つけ、取り除くかです。
エフェクトを書くと、リンターはコードに(プロパティや状態などの)リアクティブな値が含まれているかどうか確かめます。それらの値は、すべてエフェクトが読み取る依存配列に加えられていなければなりません。これにより、エフェクトはコンポーネントの最新のプロパティや状態と同期が保てるのです。要らない依存値が含まれていると、エフェクトは必要以上に実行されたり、無限ループを引き起こすかもしれません。本稿が解説するのは、エフェクトの不要な依存値をいかに見つけ、取り除くかです。
カスタムフックでロジックを再利用する
Reactには、useState
やuseContext
、useEffect
などさまざまな組み込みフックが備わっています。けれど、もっと特定の目的に応じたフックが必要になることもあるでしょう。たとえば、つぎのような用途です。
- データの取得。
- ユーザがオンラインかどうかの監視。
- チャットルームへの接続。
Reactの組み込みフックには見当たらないかもしれません。けれど、アプリケーションの必要に合わせて、フックは自作できます。