Fluent React まとめ
1章 The Entry-Level Stuff
- Reactを作ろうと思った動機は?
Webアプリケーションに求める機能が増える中、従来のページ遷移によるアプリケーションの「更新」ではなく、瞬時にUIが更新するようなUXが流行した。
webページがより早く、簡単に、大規模な即時更新を行えるようにすることはかなり難しことであった。
問題点として3つ
- パフォーマンス
DOMを操作することはブラウザがレイアウトの再計算を行うきっかけとなり、不用意なDOMの更新がパフォーマンス上のボトルネックとなることが多かった。 - 信頼性
リッチなWebアプリでは、アプリケーションの状態を追跡し、状態が一貫し正しいUIが表示できていることを保証するのは大きな課題であった。 - セキュリティ
XSSやCSRFのようなセキュリティ対策として、ページ上の全てのHTML、JavaScriptをサニタイズする必要があった。
- ReactはMVCやMVVMのような先行パターンをどのように改善しているのか?
jQueryに続くBackboneはMVCを採用し、KnockoutJSは状態を監視し依存する値を追跡するリアクティブプログラムの概念を持ち込みMVVMを実現した。
MVCでの課題とReactのアプローチ
-
状態変化管理
状態の変化を管理するためには、ビュー(V)だけでなくコントローラ(C)を変更する必要がある。他のコントローラと衝突したり、コントローラが意図しないビュー(V)を制御してしまうことがあった。また、MVCの分離は曖昧で、正確に分離することが難しいこともあった。→これに対してReactはコンポーネントのアーキテクチャをとり、UIを関数のようなものと仮定することで、状態の変化とUIへの影響について推論することが簡単になっている。コンポーネントはPropsを受け取り、出力としてUI要素を返す関数である。これによって、状態の変化や更新を単純化し、ソースコードの分離も明確になった。
-
双方向データバインディング
MVCでは双方向のデータフローを採用することがあった。これはデータの所有権を曖昧にし、懸念事項の分離が曖昧になることが多かった。
→Reactでは「一方向データフロー」と呼ばれるデータフローを採用し、UIのアップデートを制御しやすくした。また、関心ごとを明確に分離する役割も持ち、最終的にチームは高速で開発を進めることができる。
MVVMの課題に対してのReactのアプローチ
- 肥大化するビューモデル(VM)
モノリシックなビューモデルは機能やUI実装が膨らむにつれて肥大化しやすく、テストやメンテナンスを困難にさせた。
→Reactではプレゼンテーションのロジックはコンポーネントに分割され、肥大化を防いでいる。
- フラックス・アーキテクチャーの何が特別なのですか?
Fluxアーキテクチャは従来の「双方向データフロー」ではなく「単方向データフロー」によってWebアプリケーションのデータフローを制御することで、変更が予測可能になり様々な利点をもたらす。
- 信頼できる単一ソース
Fluxは信頼できる一つのソースであるStoreのみで管理する。これによりアプリケーションの動作は予測しやすく、理解しやすいものになった。複数のソースを持つことによる複雑さを回避し、バグを減らし、アプリケーション内に一貫性のある状態を保つ。 - テスト可能性
Fluxのデータフローによってアプリケーションがテストしやすいものとなっている。各部分を切り離してテストができる。 - 懸念事項の分離
システムの各部分(アクション、ディスパッチャ、ストア、ビュー)間で懸念事項が分離されている。システムはモジュール化され、メンテナンス、推論が容易になる。
- 宣言的な抽象化プログラミングの利点とは?
UI開発における宣言的な抽象化プログラミングは、ユーザーに見せたいUIを宣言するようにコーディングを行う。
宣言的な抽象化プログラミングは命令的なプログラミングと比べて、開発者体験の向上、保守性、テスト可能性が高いことがメリットとなっている。
- UIを効率的に更新するための仮想DOMの役割とは?
仮想DOMは実際のDOMを軽量なJavascriptのオブジェクトとして表現したもので、仮想DOMの変更は実際のDOMに比べて高速であることを利用し、Reactアプリケーションでの変更はまず仮想DOMの変更として処理される。
また、仮想DOMに変更が加わると、リコンシエーション)Reconciationというプロセスを経て実際のDOMへの変更が適用される。
2章 The JSX
- JSXとは?その⻑所と短所を教えてください。
- JSXとは
Javascriptの拡張構文である。
プログマ<
記号を利用し構文を解析している。JSXは_jsx``または
React.createReactElement関数として解釈される糖衣構文でもある。これらは戻り値として
ReactElement`を返し、仮想DOMとして扱われる。 - 長所
- 読みやすい
Javascript内にテンプレートを記述することができる。宣言的なUIをJavaScript内で理解しやすい形で定義することができる。 - セキュリティの向上
JSXは安全なJavascriptコードにコンパイルされるためXSSの危険性のある文字列などはサニタイズされる。より安全なテンプレートを記述することができる。 - 強力な型定義
JSXでは型付けが可能、TypescriptやJSDoc形式で型の恩恵を得ることができる。 - コンポーネント・ベースのアーキテクチャを実現
JSXはコンポーネントベースのアーキテクチャを推奨しており、コードをモジュール化し、DRY原則に従ったコーディングや、保守性の向上を実現する。 - 高い普及率
JSXはReactとは分離されたプロジェクトであり、React以外にも採用されている。他のフレームワークの独自テンプレート構文に比べて高い普及率を誇っている。
- 読みやすい
- 短所
- 学習曲線
JSXに慣れていない開発者はJSXを学分必要がある。 - ツールが必要
通常のJavascriptにコンパイルされる都合上、開発でのツールが必要となる。
(Vue.jsなどは開発ツール導入せずに動作させることが可能) - 懸念の混在
JSXはHTMLが混在し、プレゼンテーションとロジックの分離ができなくなっているという批判もある。 - インラインブロックのサポートがない
JSX内では、インライン式は利用可能だが、インラインブロックはサポートされていない。
例えば、三項演算子は使用できるがif文はサポートされていない。
- 学習曲線
- JSXとHTMLの違いは何ですか?
JSXは関数のシンタックスシュガー(糖衣構文)であり、戻り値としてJavascriptオブジェクトを返却するがHTMLは関数ではない。
- 文字列はどのようにしてマシンコードになるのか?
テキストはコンパイラを通して、正規表現によって解釈され、シンタックスツリー(AST)に変換され、構文解析、最適化、コード生成を経てマシンコードに変換される。
- JSX式とは何か、どのような利点があるのか?
JSX内の{}
で囲まれた式を指す。
これによりテンプレート内部で、三項演算子による条件チェックや文字列の置換などの式を実行することができる。
3章 仮想DOM
- DOM とは何ですか?また、仮想 DOM とどう違うのですか?
DOMはブラウザがHTMLをロードして生成するオブジェクトモデルのこと。JavascriptからブラウザのWebAPIを利用することでDOMの状態をを操作、変更でき、Viewを更新することができる。
仮想DOMはReactライブラリが生成する軽量なDOMのクローン。開発者が望むDOMの設計図とも言える。
仮想DOMはReactライブラリによって生成され、管理される。
また、JSXは仮想DOMのオブジェクトを生成するためのシンタックスシュガー(糖衣構文)とも言える。
- ドキュメントフラグメントとは何ですか?React の仮想 DOM と
の類似点と相違点は何ですか?
ドキュメントフラグメントは描画に影響与えずに、DOMの生成を行うことができる。大量の更新(リスト生成など)をバッチ処理することができる。
仮想DOMの更新アプローチはにドキュメントフラグメントと同じで、ユーザーが実装を意識せずとも、必要に応じてバッチ更新を行うことができる。
類似点は「バッチ処理」「単一レンダリング」相違点は「インターフェイスの複雑さ」「効率的な差分更新」
- DOM にはどのような問題がありますか?
問題点としては以下の2点
- DOMはアクセス、更新するたびに再描画を行う可能性があるため、アクセス方法によってパフォーマンス低下につながる場合がある。
- ブラウザによってDOM操作方法、イベントの処理方法、ネイティブイベントのアクセス方法が異なる場合がある(現在では改善されている)
- 仮想 DOM はユーザー インターフェイスの更新をより高速に実
行する方法をどのように提供しますか?
今の仮想DOMと更新後の仮想DOMを比較して実際のDOMを更新することで、不要なDOM更新をおさえている。仮想DOMを生成することだけにフォーカスできるので、開発者が細心の注意を払わずとも効率的な実装を行うことができる。
- React レンダリングはどのように機能しますか?このことからどのような潜在的な問題が生じる可能性がありますか?
コンポーネントの状態の更新があった場合、そのコンポーネントの仮想DOMを全て作り替えることになる。つまり、包含される子コンポーネントの再計算を全て行うことになり、更新がない場合も更新は行われる。
3章メモ
- JSX + 仮想DOMのアプローチは効率的なViewの更新を簡単に行うことを可能にするが、トレードオフとして仮想DOMの生成(レンダー)は都度行われることとなる。React.memoなどでパフォーマンス調整は必要。
- 仮想DOMによる(ドキュメントフラグメントのような)効率的なレンダリングが行われることと、リアクトのアプローチ(状態、props)が変化すると配下の関数(コンポーネント)を全て実行することになることに注意しなければならない。
4章 Inside Reconciliation
- React リコンシエーション(React Reconciliation)とは?
React ReconcilationとはReactの更新プロセスを行う仕組みである。コンポーネントツリーの変更を検出し、効率的に新しいツリーへと変更を適用するプロセスである。UIの更新時のパフォーマンスを最適化することを目的としている。
- Fiberノードによる分割
Fiber Reconcilerではコンポーネントの更新プロセスをFiberノードという単位で分割して整理する。これにより中断可能な更新プロセスを実現した。 - 非同期での処理が可能
これまでのStack Reconcilerでは仮想DOMで差分が検出されると、その変更を同期的に処理していた。Javascriptのcall stackを利用した処理であるため同期的であり、更新中はユーザーのインターフェース応答性が低下することがあった。Fiber Reconcilerでは更新タスクを非同期で処理することが可能になっている。 - UI応答性の改善
Stack Reconcilerでは大規模な更新があった場合、ユーザーアクションに対する反応などが遅れることがあった。
Fiber Reconcilerは、異なるタイプのアップデートを効率的にスケジュールするために、独自のタスクキューを利用する。これにより、ユーザーインタラクションのような高優先度のタスクを迅速に処理し、同時にバックグラウンドでの更新処理を適切に管理する。
例えば、画面上に映らないUIの更新やデータ処理などよりも、ユーザーのアクションに対するUIの変更を反映するようにスケジューラーが更新タスクをスケジュールする。
- ファイバー・データ構造の役割は?
Fiber Reconcilerではコンポーネントの更新プロセスをFiberノードという単位で分割して整理する。これにより中断可能な更新プロセスを実現した。
rootから始まり、参照し合うFiberノードはFiberツリーを形成する。更新が必要な要素にフラグを立てるため、Fiberツリーをルートから辿り、末端まで到達すると、必要な更新処理をスケジュールするためルートまで再帰的に処理が行われる。このような一連の処理が完了するとcurrent treeとして切り替え、スケジュールされた更新処理が実際のDOMに反映されていく。
- なぜ2本の木が必要なのか?
current treeとwork in progress treeがある。
- current Tree
現在のブラウザのUIの状態を示している。
実際にレンダリングされているコンポーネントの状態と構造であり。確定状態である。 - work-in-progress tree
Reactが新しい状態更新するために作成されるfiber tree。
2つのツリーがあるのは、ConcurrentModeにおいて更新プロセスを中断可能にするためである。
- アプリケーションが更新されるとどうなるのか?
- 仮想DOMの生成:
開発者が記述したJSXは、ReactのcreateElement関数を通じて仮想DOMノード(React要素)に変換されます。これは単にアプリケーションのUIを記述する軽量なオブジェクトです。 - Fiber Treeの構築:
仮想DOMから生成されたReact要素は、ReactのFiber ReconcilerによってFiberツリーに組み込まれます。既存のFiberツリー(current Fiber tree)があり、新しい更新が発生すると、これを基に新しいワークインプログレス(WIP)Fiberツリーが作成されます。 - 差分の計算:
WIP Fiberツリーは、現在のFiberツリーと比較されます。この比較プロセスは「リコンシリエーション」と呼ばれ、変更が必要な部分(新しいプロップス、ステート変更、コンテキスト変更など)を特定します。 - DOMの更新:
リコンシリエーションフェーズで特定された変更は、コミットフェーズで実際のDOMに適用されます。これにより、UIが新しい状態に更新されます。
NOTE
Render Phase と Commit Phase
Render PhaseではcurrentのコピーとReact Elementを基に、FiberTreeを再帰的に走査し、必要な変更をFiberNodeに記録する。
- Render Phase
後述するレンダーレーンによって優先度が高いものが現れた場合は中断され、最優先の変更を適用するためのFiberTree(Work-In-Progress Tree)を構築する。- beginWork
新しいPropsや状態の変更を基に、差分を検知しFiberNodeを走査しながらフラグを立てる。 - completeWork
更新処理の計画を行う。実際のDOMは変更されない。
- beginWork
- Commit Phase
RenderPhaseで作成されたFiberTreeを参照し実際のDOMに変更を適用するフェーズ。
RenderPhaseと違い中断はできない。
レンダーレーンについて
優先度を表すビットマスク。
変更が発生したContext(きっかけや背景)によって値が決まる。
5章 Common Questions and Powerful Patterns
- Reactにおけるメモ化とは何か、そしてそれをどのように使えばコンポーネントのレンダリングを最適化できるのか?
Reactにおけるメモ化とは、コンポーネントの不要な再レンダリングを防ぐための機能である。
Reactでは状態やPropsが変更されるとコンポーネント関数内依存するコンポーネントは全て再レンダリングを実行することになる。メモ化を行なっていない場合、Propsが変更していない場合においても再レンダリングを実行してしまい不要なレンダリングが発生する。
メモ化を行うことでコンポーネントに与えるPropsに変更がない場合は前回と同じコンポーネントを返すように振る舞わせることができる。
前提として、コンポーネントは純粋な関数であり、同じ状態、Propsに対して同じReactElementを返却するようになっている。メモ化による最適化を行うことができ、特にレンダリング時に重い処理が行われる場合、パフォーマンス最適化手段として有効である。
- Reactの状態管理にuseReducerを使う利点と、useStateとの違いは何ですか?
useReducerとuseStateはそれぞれ状態を管理するためのReact Hooksである。
- useState
- 単一の状態を管理するために使用
- 書き込み専用の関数を利用して値を設定する
- 状態の更新ロジックは抽象化されており、フローを難読化させる可能性がある
- useReducer
- 複数の状態を管理するために使用
- reducer関数を利用した状態の更新
- ロジックを分離しやすい
- テストしやすい
- イベントドリブンであるためにログを残しやすく、undo/redoや楽観的更新、ユーザーアクションの追跡などが可能
- React.lazyコンポーネントとSusppenseコンポーネントを使用して、Reactアプリケーションで遅延ロードをどのように実装できますか?
React.lazyコンポーネント/ Susppenseコンポーネント
- lazy / susppenseを利用することでコンポーネントを動的にインポートすることが可能。
<Susppense fallback={<FakeSidebarShell />}>
で軽量なコンポーネントを先に表示させ、importが完了(Promiseが解決) し、レンダリングされてから表示させるように動作させることが可能。import { lazy, Suspense, useState } from 'react'; import FakeSidebarShell from './FakeSidebarShell'; // 1kB to import const Sidebar = lazy(() => import('./Sidebar')); // 読み込み const MyComponent = ({ initialSidebarState }) => { const [showSidebar, setShowSidebar] = useState(initialSidebarState); return ( <div> <button onClick={() => setShowSidebar(!showSidebar)}> Toggle sidebar </button> <Suspense fallback={<FakeSidebarShell />}> {showSidebar && <Sidebar />} </Suspense> </div> ); };
- Reactでメモライゼーションを使用する際に発生する可能性のある問題にはどのようなものがあり、どのように軽減できるのでしょうか?
メモライゼーションについて発生する可能性のある問題について
- 「不要なメモ化」
useMemoは計算量の多い演算をメモしたり、オブジェクトや配列への安定した参照を維持するために有用であり、プリミティブなスカラー値の計算などは、メモ化のオーバーヘッドが演算量を上回るため不要と考えられます。 - 「ブラウザネイティブな要素へのイベントハンドラのメモ化」
input
やbutton
はReactのカスタムコンポーネントではなく、ブラウザネイティブな要素である。そのため仮想DOMのレンダリングは行われず、渡したPropsはそのままDOM要素に渡される。リアクトコンポーネント(純粋関数)ではないため、イベントハンドラのメモ化は不要なオーバーヘッドを与えることになる。ブラウザネイティブな要素に変更があった場合、仮想DOMはなく高速で処理されるためメモ化はかえってパフォーマンスの低下を招く。 - 「インポートのオーバーヘッド」
useMemoやuseCallbackをimportすることでインポート、呼び出し、依存関係の受け渡し、依存関係の比較処理などが呼び出されることがあるため、不要であれば利用しない選択をするべきである。
- useCallbackフックを使って、Reactのコンポーネントにpropとして渡される関数をメモする方法は?
useCallbackフックを使用してReactコンポーネントに関数をメモする方法は、useCallback
の引数に関数を与え、戻り値を利用する。これにより、依存関係配列の変数に変化がなければ、関数の参照は固定されるため再レンダリングが防がれる。
const [count, setCount] = useState(0);
const incrementCount = useCallback(() => {
setCount((prevCount) => prevCount + 1);
}, []); // Dependency array
return (<ExpensiveComponent onButtonClick={incrementCount} />)
6章 Server-Side React
- Reactアプリケーションでサーバーサイド・レンダリングを使用する主な利点は何ですか?
- SEO
CSRではクローラーが正しくwebコンテンツを会社できるとは限らない。動的なmetaデータやコンテンツ内容がJavascriptのロードや実行を必要とするため。 - パフォーマンス
CSRでは初期表示までにJavascriptの取得、解析、初期UIの構築、データの取得などをすべてクライアント上で行う。SSRでは初期UIの構築までサーバー上で行ったり追加のデータが必要なコンポーネントを後からストリームすることが可能なためパフォーマンスで有利となる。 - セキュリティ
CSRFのようあ攻撃に対しての対策がしやすい。機密データを取り扱うロジックなどもブラウザで実行される可能性がある。
- Reactでハイドレーションはどのように機能し、なぜ重要なのか?
ReactにおけるハイドレーションはSSRでレンダリングされたHTMLに対してDOMとreactをアタッチさせる処理を指す。ハイドレーション前までは、HTMLとして描画されるがDOMはreactで管理されていない状態となる。
Reactがハイドレーションを行うことで、状態管理やハンドラーと紐づけられwebアプリケーションとして機能する
- リジューム性とは何か?どのようにハイドレーションより優れていると主張しているのですか?
リジューム性(再開可能性)とは、サーバーサイドレンダリングの結果として返され、ハイドレーションを行わずにUIにインタラクティブ性を持たせることができるもの。ハイドレーションを行う従来のアプローチでは、サーバーサイドレンダリングの結果はただのテンプレートに過ぎず、ハイドレーション処理が完了されるまでボタンなどがハンドラと紐づいていない状態となる。
- クライアント専用レンダリングの主な利点と弱点は?
利点
- ページの遷移を高速で応答性の高いUXを実現する
弱点 - SEO
検索エンジンがコンテンツを正しく読み取れない可能性がある。検索エンジン、クローラーの実装は多種多様で、実際にどのようにコンテンツを読み取りインデックスされているのかを知ることは難しい。その点においてサーバーサイドで動的に生成されるものに比べて、クライアントサイドで動的にコンテンツを生成するアプローチはSEOに不利だと言える。(近年改善傾向にあるとされるが、全てのクローラーで確実とする論拠はない) - パフォーマンス
クライアントサイドのレンダリングは低速なネットワークや性能の低いデバイスではパフォーマンスの問題になる可能性がある。パフォーマンスはユーザー体験の低下、SEO、ユーザーエンゲージメントに直接影響するため、可能な限り改善する必要がある。
取得したHTMLを起点にJavascriptバンドルを読み込むクライアントサイドレンダリングのアプローチでは、HTMLのロード、Javascriptのロード、初期UIの構築、データの取得、UIのアップデートというネットワークウォータフォールが発生し、パフォーマンス最適とは言えない。 - セキュリティ
CSRFのような脆弱性対策を行うにはサーバーの制御が必要となる。
(静的サイトにおいてサーバー制御ですることでクライアントサイドレンダリングを実現することは可能であるが、そもそもサーバー制御できるのであればその他の理由からSSRを選択すべきだ。)
- ReactのrenderToReadableStreamAPIとrenderToPipeableStreamAPIの主な違いは何ですか?
renderToStringが文字列を同期的に生成して返すのに対して、下記のAPIはStreamを返す。
renderToReadableStreamAPI
Reactアプリケーションをブラウザストリームに変換する。
renderToPipeableStreamAPI
React18で導入されたサーバーサイドのレンダリングAPI。ReactアプリケーションをNode.jsストリームにレンダリングする。非同期のデータフェッチハンドリング可能にするSuspense
、Reactの同時実行をサポートしている。これらはHTML文字列ではなくReactElementのツリーをNode.jsのストリームに変換する。ストリームにより、チャンク(小さく分割)されたデータをクライアントに送ることでデータ処理のメモリ使用量が改善され、TTFBのパフォーマンスが改善される。
7章 Concurrent React
レビュー
- Reactのファイバー・リコンシラ(Fiber reconciler)とは何か。また、複雑で高性能なアプリケーションの処理にどのように貢献するのか。
fiber reconcilerとはReact.jsにおいてDOMの更新を行うプロセス、仕組みのことである。
宣言さえれたコンポーネントをFiber Nodeという単位に切り出し、Fiber Treeを形成する。Node単位で差分検知やDOM更新の計画を行うため、UIをブロッキングせずに非同期で、中断可能なレンダリングを実現している。
ダブル・バッファリング方式を使って2つのFiber Treeを使って効率的な変更を計画、実行する。一つはCurrent Tree、もう一つはWork-In-Progress Treeである。
- CurrentTree
更新適用後のDOMを表すツリーである。 - Work-In-Progress Tree
CurrentTreeのコピーを基に、コンポーネントから返されたReact.Elementから計算されるTree。このWIP Treeを作成するフェーズはRenderPhaseと呼ばれる。進行中のものより、優先度の高い更新がスケジュールされると中断され、割り込む形で処理される。CommitPhaseではWIP TreeがCurrent Treeにスワップ(昇格)され、変更を実際のDOMに適用していく
- Reactのスケジューリングと更新延期の概念について説明してください。高負荷時でもスムーズなユーザー体験を維持するために、どのように役立つのでしょうか?
- Reactのスケジューリング
ReactはイベントハンドラーなどによってPropsや状態更新された時、更新タスクをスケジュールする。更新タスクは、そのコンテキストによってレンダーレーンというビットマスクを用いてフラグを立てることで、Fiber Reconcilerが処理する順番(優先度)を整理する。 - 更新延期
ユーザーのアクションがトリガーとなる場合(クリック時のハンドラーで状態が更新される場合)、他の更新(データフェッチ結果の反映)よりも優先させることで、高いUXを実現させる。WIP Treeの構築中により高い更新タスクがスケジュールされたとき、現在のWIPTreeを破棄し、優先して高い優先度のFiber Treeを構築する。優先されたタスクが完了すると、中断されたタスクを再開(競合する場合は、はじめからWIP Treeを構築)することができる。
- Reactのレンダーレーンとはどのようなもので、更新の実行をどのように管理するのですか?レンダーレーンがビットマスキングを使用して複数の優先順位を処理する方法を説明できますか?
ReactのRender Lanesとは、Reactの更新タスクに対して割り当てられる優先度タグのようなものです。ReactはこのRender Lanesを利用して、UIの更新をどのような順番で行うべきかを判断します。複数の更新がある場合、それら全てのコンテキストを反映したビットマスクが作成されます。割り当てられたビットマスクを比較することで、より高い優先度の更新を識別できます。
論理演算によってビットマスクのマージや比較を行います。高い優先度のものから処理されます。
ビットマスキングを使用して、複数の優先度を処理する方法は以下の通りです。
- 更新のマージ
// ビットマスク const Sync = 0b00001; const LazyLoad = 0b00010; // 初期状態 let scheduledUpdates = 0; // 高優先度の同期更新をスケジュール scheduledUpdates |= Sync; // 低優先度の遅延読み込みをスケジュール scheduledUpdates |= LazyLoad; console.log(scheduledUpdates.toString(2)); // 0b0011 , SyncとLazyLoadが組み合わされる
- 評価
if (scheduledUpdates & UpdateType.Sync) { console.log('Processing synchronous updates'); // 同期更新の処理... } if (scheduledUpdates & UpdateType.LazyLoad) { console.log('Processing lazy-load updates'); // 遅延読み込みの処理... }
- ReactのuseTransitionと useDeferredValueフックの目的は何ですか?それぞれのフックが有益な状況を説明してください。
- useTransition
サンプル実装:import React, { useState, useTransition } from "react"; const PageOne = () => <div>Page One</div>; const PageTwo = () => <div>Page Two</div>; function App() { const [currentPage, setCurrentPage] = useState("pageOne"); const [isPending, startTransition] = useTransition(); const handleNavigation = (page) => { startTransition(() => { setCurrentPage(page); }); }; const renderPage = () => { switch (currentPage) { case "pageOne": return <PageOne />; case "pageTwo": return <PageTwo />; default: return <div>Unknown page</div>; } }; return ( <div> <nav> <button onClick={() => handleNavigation("pageOne")}>Page One</button> <button onClick={() => handleNavigation("pageTwo")}>Page Two</button> </nav> {isPending && <p>Loading...</p>} {renderPage()} </div> ); } export default App;
- 目的
useTransitionの戻り値であるstartTranstion
で優先度を下げるべき更新処理をラップすることで、その更新タスクは優先度の低いものとして評価されRenderLanesのビットマスクが振り分けられる。startTransition()
のコールバック内での更新は、ユーザーのアクションによって起こされるものだとしてもTransitionレーンに振られる。これによりユーザー操作によるトリガーでありながら、他の更新処理の割り込みを許可します。isPending
は遅延された更新タスク(Transition)が実行されている時trueとなり、「遅延された更新タスクが存在しない場合」はfalseを返す。 - 有用な場面
画面遷移など、画面遷移はユーザー操作によってトリガーされるが、実際の表示までは遅延される可能性がある(Susppenseを使用している場合など)。遷移中は他のユーザー入力を受け付けるようにしておくべきであることから、useTransitionを利用して、ユーザーのトリガーによる画面遷移処理の優先度を下げておく必要がある。
ユーザーのアクションがトリガーである場合レンダーレーンの評価時、その更新は優先度が高いものと評価される。そのUI更新タスクを非同期に実行し、遅延させる必要がある場合に有用。
- 目的
- useDeferredValue
サンプル実装:import React, { memo, useState, useDeferredValue } from "react"; export function App() { const [searchValue, setSearchValue] = useState(""); const deferredSearchValue = useDeferredValue(searchValue); return ( <div> <input type="text" value={searchValue} onChange={(event) => setSearchValue(event.target.value)} /> <SearchResults searchValue={deferredSearchValue} /> </div> ); } const SearchResults = memo(({ searchValue }) => { // Perform the search and render the results });
- 目的
頻繁に更新される値(例えば、ユーザー入力からの検索クエリ)の処理を遅延させることにより、UIの応答性を保ちつつ、リソースの使用を最適化する目的で使用される。
useDeferredValueの戻り値を参照するコンポーネントは一定期間古い値を表示し、他の優先度の高い更新がなければ最新の値を参照するように振舞います。 - 有用な場面
レンダリング負荷の高いコンポーネントが、頻繁に更新される可能性がある値を参照している場合、UIをブロッキングする可能性があります。このような場面で有用です。参照する値の更新を遅延させることで高い負荷の際レンダリングを抑制します。
- 目的
useTransition()
は更新タスクの優先度を下げることで、非同期に割り込みを許可する。
useDeferredValue()
は値の更新を遅らせることで、それをPropsとして参照するコンポーネントの不必要な更新頻度を抑える。
- どのような場合にuseDeferredValueを使うのが不適切なのでしょうか?これらのフックを使うことで、どのようなトレードオフがあるのでしょうか?
- 不適切な場面: リアルタイムなデータの表示が必要である場合
値の更新は別の優先度の高い更新タスクによって割り込まれる可能性があるため、古いデータで有る可能性がある。そのため、リアルタイムであることが重要な値には不向きである。
表示させるデータが「ユーザーの入力そのものであるか」がキーとなる。ユーザーの入力に対する反応を遅延させることはUXの低下につながるため不適切である。 - トレードオフ
一方で、ユーザーの直接的な操作に関連しない更新や、背景で行うデータフェッチのようなプロセスには、このフックを適用することで効率的にリソースを管理し、アプリケーションの全体的な反応性を向上させることが可能。
8章 Frameworks
質問レビュー
- Next.jsやRemixのようなReactフレームワークを使う主な理由と、それらがもたらす利点は何ですか?
- アプリケーションのスケーラビリティ、アーキテクチャについて考える時間を減らす。
- ルーティング、データフェッチ、サーバーサイド・レンダリングの一般的な解決策を提示し、車輪の再発明を防止する。
- 結果としてコードベースの品質向上し、開発者がアプリケーションの独自の部分にフォーカスすることができる。
- Reactフレームワークを使うことで生じるトレードオフやマイナス面にはどのようなものがありますか?
まず享受できるメリットについて説明する。
- 構造と一貫性
フレームワークは、コードベースの構造、パターンを強制する。一貫性が生まれるのでアプリケーションの流れを理解しやすくなる。 - ベストプラクティス
フレームワークに従うことで、ベストプラクティスを製品に組み込むことができる。
コードの質を高めて、バグを減らすことに繋がる。 - 抽象化
フレームワークは一般的なタスクを抽象する。独自のフックなどを提供し、コードを量を減らし、保守が容易になる。 - パフォーマンスの最適化
フレームワークの機能、「サーバーサイドレンダリング」「コード分割」、「静的サイト生成」などの機能を提供し、パフォーマンスやUXを改善することができる。 - コミュニティとエコシステム
人気のフレームワークには大きなコミュニティがあり、プラグインなどのエコシステムが提供されている。問題が起きたとしてもそれを解決する術が見つかりやすい。
トレードオフは以下の通り
- 学習曲線
フレームワーク固有の機能やAPIを学ぶ必要がある。 - 柔軟性と慣習
フレームワークが強制する構造や規約は、時には制約になってしまうこともあり得る。
独自の要件がフレームワークによって阻害される可能性もある。ユーザーや環境、機能が限定されている場合、フレームワークは過剰となる可能性もある。 - 依存とコミットメント
フレームワークを選択することは、アプリケーションとフレームワークが運命共同体となることである、フレームワークがメンテナンスされなくなったり、ニーズと合わないような方向に進化すると別のフレームワークに移行するなどのコストがかかる可能性がある。 - 抽象化オーバーヘッド
フレームワークは「一般的な実装」を抽象化するため、その中で何が起きているかはブラックボックスとなることが多い。デバッグやパフォーマンスチューニングを難しくし、不必要なオーバーヘッドが生じることもある。
- フレームワークが解決する一般的な問題とは?
フレームワークが解決する一般的な問題は以下の通り
- サーバーサイドレンダリング
- ルーティング
- データフェッチ
- フレームワークはそれらをどのように解決しているのだろうか?
RemixとNext.jsが一般的な問題をどのように解決しているのかを整理する。
Remix
コンセプト:Webの基本的な実装を重視している。(Formタグのactionを利用したデータ変更など)
コンポーネントは全てクライアントサイドで実行される。
基本的にサーバー側で処理を行うことで、シンプルで実装のしやすいAPIを提供している。
また、SSRのみでCDNなどエッジサイドにサーバーを配置することを推奨している。
- サーバーサイドレンダリング
サーバーサイドレンダリングの仕組みを容易にカスタマイズが可能 - ルーティング
./routes/xxxxx.tsx
のようにroutesディレクトリをルートとするフォルダパスでルーティングされる。 - データフェッチ
loader
関数(非同期)を実装しエクスポートする。 - データミューテーション
ネイティブのWebのAPIを利用しているのが特徴。action非同期関数をExportすることで、<Form action="post">で実行される。同じURLパスでサーバーへのPOSTリクエストが実行され、それをaction関数が処理する。
Next.js
コンセプト:柔軟なフルスタックフレームワーク。静的なアプリケーション(SSG)、サーバーサイドのアプリケーション(SSR)、クライアントのみのアプリケーション(CSR)を選択し制御が可能。
- サーバーサイドレンダリング
全てのページ、コンポーネントはサーバーサイドコンポーネントである。
"use client"ディレクティブをファイルのルートやコンポーネントの先頭に追加することで、クライアントで実行される。
Next.jsは小さなJSバンドルに分割しユーザーに配布し、コンテンツの大部分を静的マークアップとしてビルドし、部分的にサーバーサイドでレンダリングし、残りに部分をクライアントサイドで構築するなどを選択することが可能。 - ルーティング
./app/XXXX/page.tsx
ごとにルーティングが作成される。 - データフェッチ
サーバーコンポーネントであり、非同期に作成される。データの取得を待ってからコンポーネントがレンダリングされるように振る舞うことが可能。これはデプロイ時最初のロードで静的に生成される。 - データミューテーション
サーバーアクションという概念を利用する。フォームが送信された時などサーバー上で呼び出される関数を定義することが可能。"use server"ディレクティブを利用した関数を定義し、<form action={mutationFunction} />
というふうに指定することで、サーバー側で関数が実行される。
ノート
- フレームワークの選択基準
- プロジェクトのニーズを把握する
- プロジェクトの規模は?
- 盛り込みたい主な機能や特徴は?
- サーバーサイド・レンダリング(SSR)、サイト生成(SSG)、またはその両方の組み合わせが必要か?
- ブログやeコマースサイトのように優れたSEOの恩恵を受けそうなコンテンツの多いサイトを構築しているのか。
- リアルタイムデータや高度な動的コンテンツはアプリケーションの重要な部分か。
- ビルドプロセスのカスタマイズやコントロールに関してどの程度柔軟性が必要か。
- アプリケーションのパフォーマンスとスピードはどの程度重要か。
- Reactと一般的なwebコンセプトの習熟度は?
- ターゲットユーザーは誰か?高速なインターネット環境がある人?モバイルデバイスやネット速度が遅いユーザーもあり得るのか?
- プロジェクトのニーズを把握する
Next.js
- 最先端のReact機能を使用することがある。習得が難しい可能性もある。(サーバーコンポーネントの概念など)
- 静的コンテンツとサーバーサイドレンダリングコンテンツの間の柔軟性を持つ。
- 各ページ、コンポーネントごとに、SSG,SSR、キャッシュを組み合わせた高いパフォーマンス目指すことができる。4つの目的別のキャッシュの使い分けや、サーバー/クライアント間の境界の意識、いつどちらを使うかの意思決定の難しさなどがつきまとう。
- 静的サイトとしてプリレンダリングするため、静的サイトとして配信される。そのため表示までの速度が速い。
- Next.jsとreactのつながりは深い。
Remix
- 学習曲線がややなだらか。
- Webの基本に忠実な実装を行える。一方で、魔法のような抽象化は少なく、フラストレーションが貯まる可能性もある。
- ストリーミングを利用し、動的コンテンツのTTFBパフォーマンスを向上させる
9章 React Server Component
質問レビュー
- Reactサーバーコンポーネントの主な価値は何ですか?
React Server Component (RSC)はサーバー上でのみ実行され、クライアントのJavascriptバンドルには含まれないコンポーネントである。
- クライアントJavascriptのバンドルサイズを削減することができる。
- サーバーマシン上で実行されるためコンピュータリソースは予測可能である。
- クライアントにセキュアなデータを送信しないため、情報の漏洩の心配が少なくセキュアな操作を行える。
- サーバーコンポーネントはクライアントに渡される前に非同期データ取得などを実行することができる。
- RSCはデータをフェッチしてからレンダリングの過程をサーバー上で完結させることができる。
- クライアント・コンポーネントはサーバー・コンポーネントをインポートできますか?なぜですか?
クライアントコンポーネントではサーバーコンポーネントをインポートすることはできない。なぜなら、インポートするサーバーコンポーネントが、Node.js環境のライブラリに依存している可能性があるため。Node.jsとクライアントは環境が異なり、ライブラリやAPIがそれぞれ異なるため使用できない。
- サーバーコンポーネントと従来のクライアントのみのReactアプリのトレードオフには、どのようなものがあるのだろうか?
サーバーコンポーネントはサーバーサイドでレンダリングを行い、ReactElement(オブジェクト)を返却します。
トレードオフとして、「サーバーコンポーネントのpropsは全てシリアライズ可能であること(関数などjsonで表現できないもの)」「React Hookが使えない(useState , use Effectなど)」「UIの状態は持たないため、stateはコンポーネントのstateとして機能しない。」「クライアント・コンポーネントはサーバーコンポーネントをインポートできない」などがある。
補足:React Hookの一部が使えないのはサーバーコンポーネントが状態や副作用を実行するタイミングを持たないから。
- モジュール参照とは何ですか?また、Reactはリコンサイル時にどのようにそれを処理するのですか?
サーバーコンポーネントがクライアントコンポーネントをインポートして利用し、ReactElementがシリアライズされたとき、クライアントコンポーネントは「モジュール参照」という形で参照される。クライアントサイドでReconcile時にモジュール参照まで到達すると、そのクライアントコンポーネントを利用するようになる。サーバーコンポーネントがシリアライズされた際の一種のプレースホルダである。
- サーバーアクションは、Reactアプリをどのようにアクセスしやすくするのか?
サーバーアクションは、クライアントサイドのReactアプリケーションから、サーバーサイドの非同期関数を実行することができる。サーバーサイドでのデータ取得や処理を行うためのメカニズムである。サーバーアクションはエンドポイントとして公開され、クライアントコードのどこからでも呼び出せる。
"use server"を非同期関数と宣言することで、クライアントサイドからも呼び出されサーバー上で実行されるようになる。
async function asyncServerAction(formData) {
"use server";
const username = formData.get('username');
}
のように定義し、クライアントサイドのformタグ<form action={asyncServerAction}>
から呼び出せる。
また、useTransitionを使用して以下のようにイベントハンドラから呼び出せる。
function LikeButton() {
const [isPending, startTransition] = useTransition();
const [likeCount, setLikeCount] = useState(0);
const incrementLike = async () => {
"use server";
return likeCount + 1;
};
const onClick = () => {
startTransition(async () => {
// サーバーが返却する戻り値を読むために、awaitする。
const currentCount = await incrementLike();
setLikeCount(currentCount);
});
};
// ...
}
基本的にはReactのライブラリやフレームワークでの使用が想定されている。
10章 React Alternatives
10章ではReactの代替品について議論されている。
Vue.js、Angular、Svelte、Solid、Qwikについてそれぞれのリアクティビティ・モデル、UI開発に対するメンタルモデルについて探求する。
質問レビュー
- リアクティビティ・モデルは、React、Vue、Svelte、Solid、Angularでどのように違うのか?これらの違いが、これらのライブラリ/フレームワークのパフォーマンスや開発体験に与える影響は?
それぞれのフレームワークのリアクティビティについて、Reactを基準として比較する。
React
- リアクティビティ
リアクトのリアクティビティは「粗い粒度のリアクティビティ(coarse-grained reactivity)」であり、knockout.jsので見られる伝統的なものではない。stateやpropsが変更されると、それに反応してコンポーネント単位で再レンダリングが実行される。
また、これは直接propsやstateに依存しないコンポーネントを再レンダリングしてしまう可能性がある。 - 開発者体験
粗いリアクティビティでは、不要な再レンダリングを防止するために、React.memoやuseMemoを検討する必要がある。
また、特定のフックでは値や状態の変更に反応させるために、明示的に依存関係を宣言する必要がある。
Vue.js
- リアクティビティ
Vue3以降、signalを作成することで、「細かい粒度のリアクティビティ(fine-grained reactivity)」を実現している。ref()
ではgetter、setterを利用しその値と依存関係を追跡する、setterをトリガーに値を発火させる。reactive()
ではProxy
を利用し、オブジェクトのプロパティの依存関係を追跡し、オブジェクトプロパティの変更をトリガーに依存関係にある処理を再実行させる。
現時点で仮想DOMによる差分検知により、DOMの変更を検知するようになっている。将来的にはシグナルを - 開発者体験
明示的な依存関係の宣言を行うことなく、値に依存する処理と連動させることができる。
テンプレートベースの構文、シンプルなAPIを提供しており、シングルコンポーネントファイル(SCF)により、よりモジュール性を高めることができる。
Angular
- リアクティビティ
Zone.jsと呼ばれるライブラリを使用して、コンポーネントのビューの変更をチェックする。 - 開発者体験
複雑なアプリケーションを構築するための独自のソリューションを提供する。他のフレームワークと共通するAPIや思想が少ないために学習曲線が急になることが多い。一方で、幅広いツールやライブラリを提供しており、独自のコーディングのルールはコードベースに一貫性と構造をもたらす。
Svelte
- リアクティビティ
独自のテンプレート(.svelte)を利用し、コンパイラにより宣言的なコンポーネントを効率的な命令型コードに変換し、DOMを更新する。 - 開発者体験
変数に値を代入するだけのシンプルなインターフェースによって値の更新が可能。コンパイラによって紐づけられたDOMが更新される。
JSXや仮想DOMのアプローチで見られた、ランタイムでの動的なコンポーネントの表現は難しい。
ルーンを使用することで.svelte
外でもリアクティビティやロジックの共有を行うことができる。
エコシステムやライブラリが未成熟である。
Solid
- リアクティビティ
シグナルを利用し、きめ細かいリアクティビティを実現している。
アプローチとしてはKnockout.jsが2010年に行っていたことをシグナルによって実現しているに過ぎないが、シグナルについての議論を再活性し、他のフレームワークやライブラリに影響を与えた。 - 開発者体験
Reactと同じJSXや、類似しているAPIが使用できるため、React開発経験者とっては学習曲線が緩やか。
きめ細かいリアクティビティを実現しているため、React.memoやuseMemoについて気を払う必要がない。
エコシステムが未成熟であり、サーバーサイドレンダリングの開発が進んでいる。
- パフォーマンスを最大化するQwikのユニークなアプローチについて説明してください。これは、これまで議論してきた他のUIライブラリ/フレームワークのアプローチとどのように違いますか?
Qwik
Qwikの他のフレームワークと比較してユニークな部分は、Resumable(再開可能性)である。
サーバーサイドでレンダリングされたWebページは通常、ハイドレートを行うことでDOMとハンドラが紐づけられ、インタラクティブなアプリケーションとして動作するようになる。Qwikではハイドレーションを行わず、ブラウザがHTMLを読み込んだ時点でユーザー操作が可能であり、インタラクティブな動作が必要になったタイミングで追加のJavascriptを読み込み、実行する。その結果、初回のロード時間を最低限に抑え、より反応の良いUXを実現している。
ハイドレーションではコンポーネントをサーバー側とクライアント側でそれぞれレンダリングを行う。
- リアクティビティ
- 開発者体験
サーバーサイドレンダリングを前提としている。
Qwikはシンプルで直感的なAPIを提供しながら、JSXでコンポーネントを定義することができるためReact開発者にとっては学習曲線が緩やかと考えられる。
また、Reactとの互換性があり、既存のReactライブラリを利用することが可能。
エコシステムは未成熟ではあるがコミュニティは広がりつつある。
- この章で取り上げた各UIライブラリ/フレームワークの核となる⻑所と短所は何ですか?これらの⻑所と短所は、特定のプロジェクトのためのライブラリ/フレームワークの選択にどのように影響しますか?
React
- メリット
最も豊富な豊富なエコシステム、高い普及率、柔軟性(ライブラリ) - デメリット
仮想DOMを使ったアプローチ、粗い粒度のリアクティビティ(不要な再レンダリングの可能性、明示的な依存性宣言)
Vue - メリット
豊富なエコシステム、シンプルなDX、HTMLテンプレートベースのUI宣言、リアクティブプリミティブの採用 - デメリット
緩やかな学習曲線、仮想DOMのアプローチ
Angular - メリット
豊富なエコシステム、多機能、Googleによるサポート、HTMLテンプレートベースのUI宣言、一貫性のあるコードベース - デメリット
急な学習曲線、冗長なAPI
Solid - メリット
JSXを採用、リアクティブプリミティブの採用、高いパフォーマンス - デメリット
エコシステムが未発達
Svelte - メリット
コンパイラによる最適化、VanillaJSに近いDX、リアクティブプリミティブの採用、高いパフォーマンス - デメリット
エコシステムが未発達、動的なコンポーネントの表現が難しい
Qwik - メリット
Resumable(再開可能性)によるパフォーマンス最適化、JSXの採用、リアクティブプリミティブの採用 - デメリット
歴史が浅くエコシステムが未発達
- Reactは伝統的な意味でのリアクティブではない。VueやSvelteのようなライブラリに見られる「プッシュ・ベース」のリアクティブ・モデルと比較しながら、このステートメントを詳しく説明してください。
伝統的なリアクティブプログラミングとは、値を変更した時にその値を監視・依存(Subscribe)している処理に対して値の変更を通知(publish)し、再実行させることである。Reactの場合、useState(状態)が変更された時、そのコンポーネント全体を再レンダリングすることになり、pushベースのリアクティブと表現できる。
- React Forgetとは?どのように機能するのか?シグナルとの比較は?
React ForgetはReact.memoやuseMemoをコンパイル時に自動的に判別しメモ化することで、最適化を行う機能である。これにより、レンダリングの最適化をReactに任せることができる。
シグナルではリアクティブプリミティブに紐づく処理の身を再実行することになるので、粒度についてはReact Forgetを適用したとしてもまだ、シグナルを利用したリアクティブの方が細かい粒度と言える。