Lock in $30 Savings on PRO—Offer Ends Soon! ⏳

React 研修 (2024)

Recruit
August 09, 2024

React 研修 (2024)

2024年度リクルート エンジニアコース新人研修の講義資料です

Recruit

August 09, 2024
Tweet

More Decks by Recruit

Other Decks in Technology

Transcript

  1. React研修の目的 • Reactの「考え方」を知ってほしい • 特に「宣言的UI」の考え方 • 主に状態や副作用を扱う基本的なAPI (組込Hooks) について 使い方だけではなく「こう書くとなぜこう動くか」を理解してほしい

    • やらないこと • 本格的なハンズオン • React 18以降の新機能 • 並行レンダリング, ストリーミングSSR, React Server Components, etc. • 3rd Partyのライブラリやフレームワーク、ツール等を使う機能 • CSS, ルーティング, データフェッチ, 自動テスト, Storybook, etc. これらの一部は Next.js研修で扱います 4
  2. Webアプリ開発の変遷 • 90年代末~ • MPA (クラシックSSRのみ) • 00年代後半~ • MPA

    (クラシックSSR + jQuery) • 10年代初め~ • SPA (CSRのみ) • 10年代後半~ • SPA (CSR + 事前レンダリング) 6
  3. MPA (Multiple-Page Application) HTML ブラウザ APサーバ DOM ブクマ等から HTML HTML

    DOM DOM リンクをクリック フォームをサブミット HTMLをロードするたびに DOMツリーが構築される 8
  4. MPA (クラシックSSRのみ) • Server-Side Rendering • リンクをクリックするたび、またはフォームをサブミットするたびに サーバサイドでHTMLを動的にレンダリングする • 後述するSPAを事前レンダリングするSSR

    (SSR with Hydration) と 区別するため本研修ではMPAのためのSSRを「クラシックSSR」と呼ぶ • 生成されるWebページ自体は静的だった • 2000年代始め頃までJSはあまり活用されていなかった 9
  5. Web MVCフレームワーク • サーバサイドでは主にWeb MVCフレームワークが使われた • Model-View-Controller • 例: Ruby

    on Rails, Struts, Spring-Framework, etc. • Viewには「テンプレート」が使われた • ひな形となるHTMLに「式」を埋め込めるもの • 例: ERB, JSP, Thymeleaf, etc. • HTTPリクエスト毎にページ全体をレンダリングして返す 現在でも 広く使われています 11
  6. HTTPリクエスト HTTPレスポンス Web MVCフレームワーク C V M DB <html> <body>

    <div>${user.name}</div> </body> </html> HTML ブラウザ APサーバ ModelがDBから 取得したデータ テンプレートの中から データを参照 テンプレートはHTTPリクエスト毎に ページ全体をレンダリング Template = (data) => HTML テンプレート data 動的に生成されたHTML (ページ自体は静的) 12
  7. 2000年代のWebの変化 00 02 04 06 08 ブラウザのJSを無効に する人が多かった Webでもマイクロ インタラクション重要!

    Gmail Google Maps Flash iPhone Android インタラクティブなコンテンツの普及 Ajaxの発見 ネイティブ アプリの普及 13
  8. Webアプリ開発の変遷 • 90年代末~ • MPA (クラシックSSRのみ) • 00年代後半~ • MPA

    (クラシックSSR + jQuery) • 10年代初め~ • SPA (CSRのみ) • 10年代後半~ • SPA (CSR + 事前レンダリング) 14
  9. MPA (クラシックSSR + jQuery) • クラシックSSRが生成したHTMLと連携するJSを「後付け」 • 既存システム (クラシックSSR) に導入しやすかった

    • コンテンツ (HTML), スタイル (CSS), ロジック (JS) を 分離することがよいプラクティスだと考えられていた (過去形) • 2000年代のブラウザ環境 • IE6 (2001~) やIE7 (2006~) が主流 • JSもDOMも機能不足でブラウザ間の互換性も低かった • ブラウザの差異を吸収する高機能なライブラリが必要だった • 例: Prototype.js, Mootools, Dojo, YUI, etc. • jQueryが広く普及した 現在はjQueryを使う必要性は少なくなりましたが クラシックSSR + JSはまだ広く使われています 15
  10. jQuery • セレクタをサポートしたメソッドチェーンによるAPI • HTMLにイベントハンドラを後付けするのに適していた • イベントハンドラからDOMを操作しやすかった // DOMContentLoadedのイベントハンドラを登録 $(function()

    { // クラス属性"foo"を持つ<button />要素が押された場合のイベントハンドラ $("button.foo").on("click", function() { // クラス属性fooを持つ<p />要素を非表示にする $("p.foo").hide(); }); }); 16
  11. MPA (クラシックSSR + jQuery) DB HTML ブラウザ APサーバ Webサーバ JS

    DOM マイクロ インタラクション イベント DOMを操作 イベントハンドラ 登録 <script src="..." /> C V M 大きな画面遷移は APサーバからHTMLを 取得します 17
  12. MPA(クラシックSSR + jQuery)の課題: アプリケーションの構造 • 命令的なイベントハンドラ • DOMを参照して • 処理を行い

    • DOMを更新する • 大量のイベントハンドラが散らばる • DOMを更新する処理も散らばる • イベントハンドラ間に不明瞭な依存関係が生じる • あるイベントハンドラが動作するために前提となるDOM構造は どのイベントハンドラによって構築されるのか?破壊されるのか? • DOMが巨大で暗黙的なグローバル変数になってしまう このような構造は 「Sprinkle」と 呼ばれることがあります 18
  13. MPA(クラシックSSR + jQuery)の課題: ワークフロー • 「HTML」はマスタとなるリソースではない • Gitでバージョン管理されるリソースは「テンプレート」 • 例:

    ERB, JSP, Thymeleaf, etc. • JSの処理対象となるHTMLはAPサーバの実行結果 • JS側が必要とするHTMLの修正を誰がどのように行うか? • テンプレートの修正が必要 • 通常はサーバサイドの開発者が担当するリソース • フロントエンド側とは開発サイクルも開発のワークフローも異なることが多い • 二重開発 • サーバサイドのテンプレートとフロントエンドのJSが機能的に重複 20
  14. MPA(クラシックSSR + jQuery)の課題: ワークフロー DB HTML ブラウザ APサーバ Webサーバ JS

    DOM テンプレート C V M フロントエンド側で 開発するリソース 開発リソースではない 依 存 サーバサイド側で 開発するリソース 齟齬 スケジュールの違い jQueryを使わなくても クラシックSSR + JSは 同じ課題を抱えています 二重開発 21
  15. Webアプリ開発の変遷 • 90年代末~ • MAP (クラシックSSRのみ) • 00年代後半~ • MPA

    (クラシックSSR + jQuery) • 10年代初め~ • SPA (CSRのみ) • 10年代後半~ • SPA (CSR + 事前レンダリング) 22
  16. SPA (CSRのみ) • Single Page Application • ナビゲーションによる画面遷移もブラウザ上のJSでレンダリング • 単一のHTMLページ

    (あるいはDocument) だけで構成されるためSPA • ナビゲーションの単位も「ページ」と呼ぶので紛らわしい • 例: トップページ、一覧ページ、商品ページ、etc. 23
  17. SPA (Single-Page Application) HTML ブラウザ サーバ DOM ブクマ等から JS リンクをクリック

    フォームをサブミット ロードされるHTMLページは一つだけ (Documentオブジェクトは不変) イベント イベント DOM更新 DOM更新 JSON JSON 24
  18. SPA (CSRのみ) の起動シーケンス DB 静的 HTML APIサーバ Webサーバ JS DOM

    インタラクション イベント レンダリング レンダリング ブラウザ JSON JSON <script src="..." /> (CSR) (CSR) コンテンツとしては空 (SSR不要) 28
  19. SPA向けフレームワーク • MV*フレームワークの登場 • MVC, MVP, MVVM, MVAC, etc. の総称

    (*はワイルドカード) • 例: Backbone.js, AngularJS, Ember.js, Knockout.js, etc. • 当初はBackbone.js、後にAngularJSが主流になりそうだった • 10年代半ば以降はMV*フレームワークではないReact(後述)と (特に日本では) VueJSが主流になった • MPA (クラシックSSR + jQuery) の課題を解決 • フロントエンドのアプリケーションに構造がもたらされた • Model, View, Controller, Presenter, View Model, etc. • DOMのグローバル変数化および「Sprinkle」からの脱却 • サーバサイドのテンプレートが不要になった • フロントエンドのリソース (HTML, CSS, JS) はフロントエンドで完結 29
  20. イベント クラシックSSR + jQuery イベント MV*フレームワーク 更新要求 変更通知 MV*フレームワークと状態 DOM

    状態 イベントハンドラ DOM Controller Model 状態 参照 更新 View 更新 具体的な構造は MV*フレームワークによって 異なります 30
  21. SPA (CSRのみ) の課題 • MPA (クラシックSSR) で構築されたサイトからの移行が困難 • 現在もMPA (クラシックSSR

    + jQuery) が健在な理由 • SEO対策が困難 (10年代半ばのクローラーはJS非サポート) • 現在はGoogleなどJSをサポートしたクローラーもある • しかしインデクシングされるまでに時間がかかることもあり現在でも不利 • SEOが不要なサービスでは課題にならない • OGP対応が困難 • 初期表示 (LCP/FMP) が遅い • 最初に読み込まれるHTMLがコンテンツを含まず、JSが実行されてから コンテンツが表示されるため • Largest Contentful Paint • First Meaningful Paint 31
  22. SPA (CSRのみ) の初期表示 DB 静的 HTML APIサーバ Webサーバ JS DOM

    レンダリング ブラウザ JSON コンテンツとしては空 (SSR不要) <script src="..." /> (CSR) コンテンツは空 コンテンツあり LCPが遅い 33
  23. 初期のMV*フレームワークの課題 • Backbone.jsの課題 • Viewのサポートが手薄 • 結局jQueryと組み合わせて使うことが多かった • 初期表示はテンプレート (Handlebars等)

    を使い、更新はjQuery (命令的) を使う等 • AngularJSの課題 • Dependency Injection (DI) 等を含む多機能なフレームワークのため初 期の学習コストが高いと見られやすかった • 複雑な画面になると表示パフォーマンスが劣化した 34
  24. Webアプリ開発の変遷 • 90年代末~ • MPA (クラシックSSR) • 00年代後半~ • MPA

    (クラシックSSR + jQuery) • 10年代初め~ • SPA (CSRのみ) • 10年代後半~ • SPA (CSR + 事前レンダリング) 35
  25. SPA (CSR + 事前レンダリング) • 最初に配信されるHTMLにコンテンツを事前レンダリングする • 事前レンダリングにはCSRと同じライブラリ/コードを使う • 例:

    React, VueJS, etc. • 事前レンダリングではDOM操作の代わりにHTML文字列を生成 • Isomorphic JSまたはUniversal JSとも呼ばれた • 事前レンダリングの実行にはサーバサイドのJSランタイム (主にNode.js) が使われる • 初期表示の後はSPA (CSRのみ) と同様にCSRで画面を更新 • 事前レンダリングはSPAの初期表示のための最適化 • 事前レンダリングの課題 • INP (Interaction to Next Paint) が遅い React 18のStreaming SSRや React Server Components (RSC) で 解決すると期待されますが本研修では扱いません 「事前」はブラウザに 読み込まれるよりも 前という意味です 36
  26. 事前レンダリングの種類 • ページ単位で事前レンダリングの方式を選択できる • (メタ) フレームワーク (後述) によって異なる • SSR

    (Server-Side Rendering) • HTTPリクエスト時にオンデマンドで事前レンダリング • ランタイムのサーバ (Node.js等) が必須 • SG (Static Generation) • ビルド時に事前レンダリング • ランタイムのサーバ (Node.js等) を不要にできる • ISR (Incremental Static Regeneration) • SGとSSRの組み合わせ (事前ビルド + オンデマンド) • ランタイムのサーバ (Node.js等) が必須 MPAのクラシックSSRと 区別する場合は SSR with Hydration とも呼ばれます Static Site Generation (SSG)とも呼ばれます 37
  27. SPA (CSR + 事前レンダリング) DB HTML APIサーバ Node.js JS DOM

    レンダリング ブラウザ <script src="..." /> (Hydration) LCPが速い JS コンテンツを 含む JSON 同じコード 同じ コードベース JSON レンダリング (CSR) イベント インタラクション INPが遅い コンテンツあり これはSSRの例 38
  28. (メタ) フレームワーク • React等をベースに事前レンダリング、ルーティング、 データフェッチ等の機能を付加したもの • Reactベースの (メタ) フレームワーク •

    例: Gatsby, Next.js, Remix, etc. • React以外をベースとする (メタ) フレームワーク • 例: Nuxt (VueJS), Angular Universal, SvelteKit, SolidStart, etc. • 呼称について • React等をライブラリと呼ぶ場合 • Next.js等をフレームワークと呼ぶことが多い • React等をフレームワークと呼ぶ場合 • Next.js等をメタフレームワークと呼ぶことが多い React公式 ドキュメントはこちら 39
  29. 広がるフロントエンドの領域 ブラウザ Node.js & Next.js バックエンド DB フロントエンドチームが開発運用するサーバを Backend for

    Frontend (BFF) と呼ぶことがあります インターネット LAN クライアント サーバ フロントエンド バックエンド 40
  30. Webアプリ開発の変遷 まとめ • 現代の典型的なWebアプリ • MPA (クラシックSSR + jQueryまたはVanilla JS)

    • 古くからある既存システムとその周辺のサービスに多い • SPA (CSRのみ) • SEOも初期表示の速度も要求されない新規サービスで選ばれやすい • SPA (CSR + 事前レンダリング) • SEOや初期表示の速度が要求される新規サービスで選ばれやすい 41
  31. Webアプリ開発の変遷 まとめ • Webアプリ (フロントエンド寄り) 開発者の立ち位置 • クラシックSSRのテンプレート開発者 • サーバサイドのテンプレート記述言語

    (ERB, JSP, Thymeleaf, etc.) を利用 • クラシックSSRが生成したHTMLと共に動作するJSの開発者 • 主にjQueryまたはVanilla JSを利用 • SPAのJS開発者 • SPA用のライブラリやフレームワークを利用 • 主にReact, VueJS • 事前レンダリングを行う場合は (メタ) フレームワークも利用 • 主にNext.js, Nuxt • ブラウザに留まらずサーバサイド (BFF) もフロントエンドの領域に 42
  32. Reactとは • UI構築のためのライブラリ • 主にSPAの開発に使われる • MPAやネイティブアプリの開発に使うこともできる • 開発元はMeta (旧Facebook)

    • 2011年からFacebook内部で利用開始 • 2013年にOSSとして公開 • https://github.com/facebook/react • TypeScriptではなくFlowtypeで記述されている • TSの型定義はコミュニティ (Definitely Typed) より提供されている 44
  33. Reactの特徴 • Viewに特化 • コンポーネント指向 • JSX • 宣言的UI •

    仮想DOM (Reactツリー) • Learn Once, Write Anywhere 45
  34. Viewに特化 • MV*フレームワークではない • そのためReactは「ライブラリ」と自称している • メリット • 小さく始めやすい •

    間違った (早すぎる) ベストプラクティスを押しつけない • 3rd Partyライブラリが活発に開発されてエコシステムが充実 • デメリット • Viewレイヤ以外をどうするかはアプリ開発側で考える必要がある 46
  35. Viewレイヤ以外は? • 当初はMV*フレームワークとの組み合わせが試された • Backbone.jsなど • Facebook自身もMV*の一種「Flux」アーキテクチャを提唱 • Fluxアーキテクチャを実装したOSSフレームワークが多数リリース •

    Flux戦争と呼ばれた • 実際はアプリレベルでMV*アーキテクチャは不要だった • MV*以外にも必要なものはある • ルーター、データフェッチ、ステート管理、フォーム管理、etc. • OSSエコシステムの競争から勝者 (デファクト) が決定 • Next.js等の (メタ) フレームワークである程度解消 • それでも足りないものはアプリ開発者自身で選択する 47
  36. コンポーネント指向 • Reactアプリの構成要素はコンポーネント • ModelやController等は不要 • コンポーネントが持つもの • 表示のためのロジックやイベントハンドラ •

    状態 • コンテンツ (マークアップ) • コンポーネントは子コンポーネントを持つことができる • コンポーネントのツリーを構築する 48
  37. MV*とReactコンポーネント DOM View ViewModel Model コンテンツ ロジック 状態 更新 バインディング

    変更通知 MV* React DOM Component 更新 ロジック 状態 コンテンツ 具体的な構造は MV*フレームワークによって 異なります Component ロジック 状態 コンテンツ Component ロジック 状態 コンテンツ 子コンポーネント (複数可) 子コンポーネント (複数可) 49
  38. JSX • JSにマークアップ (HTML) を埋め込む構文 • 実際はHTMLではなくXML風の構文 • ツールによってJSの式に変換される •

    ReactとJSXは独立している • JSXを使わずにReactを使うこともできる • React以外でJSXを使うこともできる 54
  39. JSX export default function App() { ... return ( <div>

    <Header /> <Main /> <Footer /> </div> ) } JSX 55
  40. JSX • 当初は不評だった • コンテンツ (HTML), スタイル (CSS), ロジック (JS)

    を 分離することがよいプラクティスだと考えられていたため • 後に間違った「Separation of Concerns」だったとみなされるように変化 • 現在では広く受け入れられている • Babel, TS, VSCode等のツールによる幅広いサポート • React以外のUIライブラリでもサポートされている • 例: VueJS, SolidJS, Qwik, etc. 56
  41. 宣言的UI • 宣言的 • whatを記述する • 選択中のタブがn番目ならXXXを表示する • 状態に対して一意に定まるUIを定義する •

    UI = f(state) • 対義語は命令的 • howを記述する • jQueryは命令的になりがち (特に更新) • n番目のタブがクリックされたら、 • 現在選択中のタブがn番目でないことを確認し、 • タブの下のパネルに他のタブ用の要素がもしあれば削除し、 • パネルに新しい要素を追加し、属性を設定し、子要素を追加し、 テキストを設定し、…… jQueryを使わない Vanilla JSでも同様に 命令的になりがちです 57
  42. 宣言的UI • Reactのメンタルモデル • 状態が変わる毎にコンポーネントを毎回実行してDOMを新規に構築 • コンポーネント = (state) =>

    DOM • 毎回新規にレンダリングするのと同等 • 画面の更新について考えることが激減 • クラシックSSRのテンプレートに近いメンタルモデル • クラシックSSRではHTTPリクエストのたびに毎回テンプレートを 実行してHTMLを生成する • テンプレート = (data) => HTML • 画面の更新については考える必要がない • 更新はブラウザ側のjQuery (またはVanilla JS) の仕事 押しつけられた側は 命令的でとても大変 58
  43. 仮想DOM (Reactツリー) • Reactのメンタルモデル • 状態が変わる毎にコンポーネントを毎回実行してDOMを新規に構築 • 現実のDOMは遅い • 特に更新が遅い

    • メンタルモデルそのままではパフォーマンスが実用的にならない • 仮想DOM • DOMの代わりにJSのオブジェクト (軽量) で仮想的なDOMを構築 • コンポーネント = (state) => VDOM • 差分更新 • 前回レンダリングした時の仮想DOMと新しい仮想DOMを比較 • 差分だけを (実) DOMに反映 59
  44. 仮想DOMの動作イメージ (1) A B C <div> <span> <span> "Foo" "Bar"

    <div> <span> <span> "Foo" "Bar" 最初のレンダリング コンポーネント 仮想DOM (実) DOM ①実行 ②反映 60
  45. 仮想DOMの動作イメージ (2) A B C <div> <span> <span> "Foo" "Bar"

    <div> <span> <span> "Foo" "Bar" 最初のレンダリング コンポーネント 仮想DOM (実) DOM ①実行 ②反映 レンダーフェーズ コミットフェーズ 広義のレンダリング 61
  46. 仮想DOMの動作イメージ (3) A B C <div> <span> <span> "Foo" "Bar"

    <div> <span> <span> "Foo" "Bar" A B C <div> <span> <span> "Baz" "Bar" 最初のレンダリング 再レンダリング コンポーネント 仮想DOM (実) DOM ①実行 ④実行 ②反映 ③状態が更新 62
  47. 仮想DOMの動作イメージ (4) A B C <div> <span> <span> "Foo" "Bar"

    <div> <span> <span> "Foo" "Bar" A B C <div> <span> <span> "Baz" "Bar" 最初のレンダリング 再レンダリング コンポーネント 仮想DOM (実) DOM ①実行 ④実行 ②反映 ⑤比較 ③状態が更新 63
  48. 仮想DOMの動作イメージ (5) A B C <div> <span> <span> "Foo" "Bar"

    <div> <span> <span> "Foo" "Bar" A B C <div> <span> <span> "Baz" "Bar" <div> <span> <span> "Baz" "Bar" 最初のレンダリング 再レンダリング コンポーネント 仮想DOM (実) DOM ①実行 ④実行 ②反映 ⑥差分を反映 ⑤比較 ③状態が更新 64
  49. 仮想DOM (Reactツリー) • 仮想DOMは最適化の1つ • 宣言的UIのメンタルモデルと実用的なパフォーマンスを両立 • 両立する手段は仮想DOMだけではない • 近年は「Signals」をサポートするUIライブラリが増加している

    • 「仮想DOMは速い」は (必ずしも) 正しくない • 「仮想DOMは (それほど) 遅くならない」程度が適切 • 仮想DOMの構築を減らすためのパフォーマンスチューニングが 必要になることもある • 現在のReactは「仮想DOM」とは呼ばない • DOMを使わない環境 (React Native等) も存在するため • ドキュメント上は「Reactツリー」と呼ばれている • 差分検出処理のことは「Reconciliation」と呼ばれる 本研修では「仮想DOM」 を使います 65
  50. Learn Once, Write Anywhere • ReactはWebアプリ開発だけのものではない • React Native •

    ネイティブアプリ開発用 • iOS, Android, Windows, Mac, etc. • 一度Reactを学習すればWebもネイティブも書ける • Reactのパッケージ構成 • react • Web向け・ネイティブ向け共通のパッケージ • react-dom • Web向けのパッケージ • react-native • ネイティブ向けのパッケージ 本研修では React Nativeは 扱いません 66
  51. Reactのパッケージ構成 react react-dom react-native Browser iOS Android Windows MacOS Browser

    … react- native- windows react- native- macos react- native-web … Webアプリ iOS アプリ Android アプリ Windows アプリ MacOS アプリ Web アプリ 67
  52. 演習(2-1): StackBlitz • StackBlitzを開いてみよう • https://stackblitz.com/ • GitHubアカウントでサインインしよう • 画面右上の「Sign

    in」をクリック • 「Continue with GitHub」をクリックしてサインイン • 新しいProjectを作成しよう • 画面左上の「+New Project」ボタンを押す • 「Add to ~」モーダルが開く • 「React TypeScript」を選択 • 新しいProjectが開くのでソースを確認してみよう GHEではなく github.comのアカウントです 68
  53. index.html <!doctype html> <html lang="en"> <head> <meta charset="UTF-8" /> <link

    rel="icon" type="image/svg+xml" href="/vite.svg" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Vite + React + TS</title> </head> <body> <div id="root"></div> <script type="module" src="/src/main.tsx"></script> </body> </html> 最初に読み込まれる HTML 70
  54. src/main.tsx import React from 'react' import ReactDOM from 'react-dom/client' import

    App from './App.tsx' import './index.css' ReactDOM.createRoot(document.getElementById('root')!).render( <React.StrictMode> <App /> </React.StrictMode>, ) Reactアプリの エントリポイント ルートとなるDOM要素に Reactアプリをレンダー TypeScriptでJSXを使うには ファイルの拡張子を.tsxにします これ以降はReactが 制御を握ってアプリを 呼び出す 71
  55. src/App.tsx ... function App() { const [count, setCount] = useState(0)

    return ( <> ... </> ) } export default App <div>にマウントされる Appコンポーネント JSX この部分のテキスト(文言)を 自由に書き換えてみよう! 72
  56. React概要 まとめ • Reactは • UI構築のためのライブラリ • MV*フレームワークではなくViewに特化 • コンポーネント指向、JSX

    • 宣言的UI • 状態が変化するたびに初回表示のようにコンポーネント全体を再実行する メンタルモデル • 仮想DOM (Reactツリー) • 宣言的なメンタルモデルとパフォーマンスを両立 • Learn Once, Write Anywhere • Webアプリだけではなくネイティブアプリも開発可能 73
  57. コンポーネント • Reactアプリの構成単位 • コンテンツ、ロジック、状態を持つ • コンポーネントの実装方法 • クラスコンポーネント •

    現在はほとんど使われない • 関数コンポーネント • 現在の主流 本研修では クラスコンポーネントは 扱いません 75
  58. 関数コンポーネント • JS/TSの普通の関数として実装するコンポーネント • 関数名の先頭は大文字 • 引数 • 親コンポーネントから渡されるオブジェクト (Props)

    • 戻り値 • 主にReactElement • ReactElementを構築するためにJSXを使う • null, undefined, boolean, number, string, 関数, 配列 • 上記をまとめたReactNode型 76
  59. 関数コンポーネントの書き方 type Props = { name: string; }; export const

    Message: React.FC<Props> = ({name}) => { return <div><span>Hello, {name}!</span></div>; }; アロー関数式で記述 functionキーワードを使った 関数式はあまり使われない type Props = { name: string; }; export default function Message({name}: Props) { return <div><span>Hello, {name}!</span></div>; } 関数宣言 (文) で記述 最近の主流 React.FC<Props>は @types/reactで定義された型 77
  60. JSX • JS XML • JSの式としてXML風の構文を記述できる • BabelやTS (tsc) 等のツールによりJSの式に変換される

    (Alt JS) import React from "react"; import { Child } from "./Child"; export function App() { return ( // ←の括弧がないと;が挿入される (ASI) <div className="foo"> <span>Hello, React!</span> <Child name={"Foo"} /> </div> ); } import React from "react"; import { Child } from "./Child"; export function App() { return ( React.createElement("div", { className: "foo" }, React.createElement("span", null, "Hello, React!"), React.createElement(Child, { name: "Foo" }))); } 現在はよりコンパクトなJSに トランスパイルされます 78
  61. JSXとHTMLの違い • JSXはXML風の構文であって通常のHTML構文と同じではない • かつてのXHTMLに近い • 現在のHTML Living Standardでは「The HTML

    Syntax」よりも 「The XML Syntax」に近い • 特に属性名はHTML Living StandardでWeb IDLにより定義される DOMインタフェースの属性名が採用されている 79
  62. JSXとHTMLの違い (要素) • タグ名は小文字と大文字を区別する • タグ名の先頭が小文字ならDOM要素にマッピングされる • 例: <div>...</div> •

    タグ名の先頭が大文字ならReactコンポーネントにマッピングされる • タグ名は関数への参照として解決できなくてはならない • 例: <Button>...</Button> • ピリオド区切りでJSオブジェクトのメンバーを参照することができる • タグ名の先頭が小文字でもReactコンポーネントにマッピングされる • 例: <React.Suspense>...</React.Suspense> • 終了タグは省略できない • 例: <li>...</li>, <input /> DOM要素にマッピングされる コンポーネントは 「ホストコンポーネント」 と呼ばれることがあります 80
  63. JSXとHTMLの違い (属性名) • 小文字と大文字を区別する • 主にキャメルケースを使う • 例: <a referrerPolicy="origin">...</a>

    • 例外: data-*属性, aria-*属性 • HTMLと異なる属性名がある • class → className • for → htmlFor HTML Living Standardの DOMプロパティ名に 近いです (一部例外あり) 属性名はJSX仕様ではなく react-domのAPIで 決められています この他にフォームや CSSに関連する属性に 違いがあります (後述) 81
  64. JSXとHTMLの違い (属性値) • 属性値は一重または二重引用符 で囲む (省略不可) • 例: <input type="checked"

    /> • 属性値にJSの式を使うこともできる • 例: <input value={text} /> • 論理属性にはboolean型のJS式を使うことができる • 例: <input type="checkbox" checked={flag} /> • 論理属性がtrueの場合は属性値を省略できる (HTMLと同様) • 例: <input type="checkbox" checked /> JSXの中でJSの式を 使う方法は後述 82
  65. ルート要素 • JSXの式は単一のルート要素を持つ • ルート要素として対応するHTML要素を書けない場合は 「フラグメント」要素を使う • <>...</> • または<React.Fragment>...</React.Fragment>

    import React from "react"; export function ListItem() { return ( <dt>タイトル</dt> <dd>説明</dd> ); }; 間違い import React from "react"; export function ListItem() { return ( <> <dt>タイトル</dt> <dd>説明</dd> </> ); } 後述するkey属性を 指定する場合はこちら 83
  66. JS in JSX • JSXの中にJSの式を記述することができる • JSX構文の中でJSの式を使うにはJSの式を波括弧 {} で囲む •

    JSの式は属性値にも要素の内容にも利用できる • JSとJSXは任意にネストできる export default function App() { const id = "abc"; const flag = !Math.floor(Math.random() * 2); return ( <div id={id + "def"}> <div>{flag ? <span>Foo! {random}</span> : <span>Bar! {random}</span>}</div> </div> ); } 黄色の文字はJSX 白色の文字はJS 84
  67. 演習(3-1): JSの値をJSXで表示 • Stackblitzで新しいProjectを作ってみよう • JSXの中からJSの様々な値をレンダリングしてみよう • プリミティブ値 • undefined,

    null, boolean, number, string, etc. • オブジェクト • 普通のオブジェクト, 配列, 関数, 正規表現, Date, etc. export default function App() { return ( <ul> <li><span>undefined</span>:<span>{undefined}</span></li> <li><span>null</span>:<span>{null}</span></li> ・・・ </ul> ); } numberやstringは 様々な値を 表示してみよう Projectの名称を [3-1] JSX などに変更しよう 85
  68. JS値とテキストノード • JSXのテキストノードに書かれたJSの値は次のように扱われる • 表示されない • undefined, null, boolean, bigint,

    symbol, 関数 (警告が出る) • 表示される • string • 一部の文字 ('<', '>', etc.) はエスケープされる • 表示されるが実用的でない • number • 通常はアプリでのフォーマットが必要 • 実行時エラー • 関数と配列以外のObject • 配列 • 各要素について上記が適用される (区切り文字なしで連結) 86
  69. JSXと制御構造 • JSX自体は制御構造を提供しない • JSの制御構造と組み合わせることができる • JSX中では主に式を使う • 分岐 •

    論理演算子, 条件演算子 • 論理演算子はboolean値が表示されないことを利用する • 反復 • Array.prototype.map(), etc. 87
  70. JSXと制御構造 export default function App() { const flag = !Math.floor(Math.random()

    * 2); return ( <div>{flag && <span>true!!</span>}</div> ); } 論理演算子による分岐 export default function App() { const flag = !Math.floor(Math.random() * 2); return ( <div>{flag ? <span>true!!</span> : <span>false!!</span>}</div> ); } 条件演算子による分岐 88
  71. JSXと制御構造 export default function App() { const numbers = [1,

    2, 3, 4, 5]; return ( <ul> {numbers.map((n) => ( <li> <span>{n}</span> <span>{n % 2 ? "odd" : "even"}</span> </li> ))} </ul> ); } Array.prototype.map()による繰り返し 89
  72. 繰り返しとkey属性 • 前頁の例を実行すると警告が出力される • 繰り返される要素にはkey属性が必要 • 繰り返し中における個々の要素をReactが識別できるようにするため • 仮想DOMによる差分検出処理で使われる •

    key属性の値は安定した値が望ましい • 商品一覧における商品であれば「商品コード」など • 配列のインデックスは極力避ける Warning: Each child in a list should have a unique "key" prop. Check the render method of `App`. See https://reactjs.org/link/warning-keys for more information. at li at App 90
  73. 繰り返される要素の差分検出 (keyなし) <ul> <li>Red</li> <li>Green</li> <li>Blue</li> <ul> 先頭に要素を追加 差分検出 DOM更新

    更新量が 多い! <li>Red</li> <li>Green</li> <li>Blue</li> <li>White</li> <ul> <li>Red</li> <li>Green</li> <li>Blue</li> <li>White</li> 仮想DOM 91
  74. 繰り返される要素の差分検出 (keyあり) <ul> <li key="00FF00">Red</li> <li key="008000">Green</li> <li key="0000FF">Blue</li> <ul>

    先頭に要素を追加 差分検出 DOM更新 更新量が 少ない! <li key="00FF00">Red</li> <li key="008000">Green</li> <li key="0000FF">Blue</li> <li key="FFFFFF">White</li> <ul> <li key="00FF00">Red</li> <li key="008000">Green</li> <li key="0000FF">Blue</li> <li key="FFFFFF">White</li> 仮想DOM 92
  75. 演習(3-2): Todoアプリ • StackBlitzで新しいProjectを作成してみよう • src/App.tsxを修正してTodoのリストを表示してみよう const todoList = [

    { id: 1, task: "Learning Browser", completed: true }, { id: 2, task: "Learning JavaScript/TypeScript", completed: true }, { id: 3, task: "Learning React", completed: false }, { id: 4, task: "Learning Next.js", completed: false }, ]; export default function App() { return ( // ここを埋めてください // completedの表示には<input type="checkebox" />を使ってください // この時点ではチェックボックスは更新できないので<input>要素にreadOnly属性を指定してください ); } Projectの名称を [3-2] Todo などに変更しよう 93
  76. Props • 子コンポーネントは引数でデータを受け取ることができる • この引数 (オブジェクト) をPropsと呼ぶ • Propsの各プロパティは親コンポーネントが渡した属性 •

    Propsはイミュータブルとして扱うこと • 特殊なProps • children: 親コンポーネントにおいてJSX要素の属性ではなく、 開始タグと終了タグの間に記述された子ノードの配列 • key: 子コンポーネントには渡らない • 将来のReactでは通常のプロパティになる予定です • ref: 子コンポーネントが受け取るにはforwardRef()が必要 • 将来のReactでは通常のプロパティとなり、 forwardRef()は 不要となる予定です 「状態と再レンダリング」 で説明します 後述します 95
  77. コンポーネントとProps export type Props = { name: string; }; export

    function Child({ name }: Props) { return <span>{name}</span>; } import { Child } from "./Child"; export function Parent() { return ( <div> <Child name="Foo" /> <Child name="Bar" /> </div> ); } 子コンポーネント 親コンポーネント 96
  78. 仮想DOMとコンポーネント • 関数コンポーネントにはクラスにおける「インスタンス」は 存在しない • 関数コンポーネントそのものは状態を持たない • ステートレス • しかしReactはコンポーネントの情報を仮想DOMの一部として管

    理している • React内部では「Fiber」と呼ばれるデータ構造で管理している • 関数コンポーネントはFiberに保持される情報と紐付けられる • Props, State, etc. 97
  79. 仮想DOMとコンポーネント Parent { } export function Parent() { return (

    <div> <Child name="Foo" /> <Child name="Bar" /> </div> ); } export function Child({name}) { return <span>{name}</span>; } ReactDom.createRoot(rootElement).render(<Parent />) ①作成 React アプリ 仮想DOM Props コンポーネントを 管理する情報 98
  80. 仮想DOMとコンポーネント Parent { } export function Parent() { return (

    <div> <Child name="Foo" /> <Child name="Bar" /> </div> ); } export function Child({name}) { return <span>{name}</span>; } ReactDom.createRoot(root).render(<Parent />) ②実行 React アプリ 仮想DOM 99
  81. 仮想DOMとコンポーネント Parent { } export function Parent() { return (

    <div> <Child name="Foo" /> <Child name="Bar" /> </div> ); } export function Child({name}) { return <span>{name}</span>; } ReactDom.createRoot(root).render(<Parent />) React アプリ Child { name: "Foo" } Child { name: "Bar" } ③作成 Props Props ホストコンポーネントは省略 仮想DOM 100
  82. 仮想DOMとコンポーネント Parent Child { name: "Foo" } Child { name:

    "Bar" } { } export function Parent() { return ( <div> <Child name="Foo" /> <Child name="Bar" /> </div> ); } export function Child({name}) { return <span>{name}</span>; } ReactDom.createRoot(root).render(<Parent />) ④実行 React アプリ Props Props 仮想DOM 101
  83. 仮想DOMとコンポーネント Parent Child { name: "Foo" } Child { name:

    "Bar" } { } export function Parent() { return ( <div> <Child name="Foo" /> <Child name="Bar" /> </div> ); } export function Child({name}) { return <span>{name}</span>; } ReactDom.createRoot(root).render(<Parent />) ⑤実行 React アプリ Props Props 仮想DOM 102
  84. (広義の) コンポーネント { name: "Foo" } { name: "Bar" }

    { } export function Parent() { return ( <div> <Child name="Foo" /> <Child name="Bar" /> </div> ); } export function Child({name}) { return <span>{name}</span>; } export function Child({name}) { return <span>{name}</span>; } 関数コンポーネントは Reactが管理する文脈の内側で 呼び出されるイメージ Reactが管理する情報と 関数コンポーネントを結びつけて 雑に「コンポーネント」と 呼ぶ場合がある Props 仮想DOM 103
  85. 演習(3-3): Todoアプリ • Todoのアイテムを一つだけ表示するTodoItemコンポーネントを 作成し、Appコンポーネントから使ってみよう import { TodoItem } from

    "./TodoItem"; export type TodoItemType = { id: number; task: string; completed: boolean; }; const todoList: TodoItemType[] = [...]; export default function App() { return ( ... <TodoItem ... /> ... ); } src/App.tsx import { type TodoItemType } from "./App"; type Props = { todoItem: TodoItemType; }; export function TodoItem(props: Props) { return ( ... ); } src/TodoItem.tsx 演習3-2のProjectを開いて 左上の「Fork」で 新しいProjectを作成しよう ディレクトリツリーの「src」に マウスカーソルを重ねると ファイルの追加ができます Projectの名称を [3-3] Todo などに変更しよう 104
  86. childrenプロパティ export type Props = { children: React.ReactNode; }; export

    function Layout({ children }: Props) { return ( <div> {children} </div> ); } import { Layout } from "./Layout"; export function Parent() { return ( <Layout> <div>...</div> テキスト <div>...</div> </Layout> ); } 子コンポーネント 親コンポーネント 106
  87. イベントハンドラ • JSXでHTML要素に対してイベントハンドラを設定できる • onClick等の属性に関数を渡す • 例: <button onClick={(event) =>

    {...}}>...</button> • キャプチャフェーズはイベント名の末尾にCaptureを付ける • 例: <button onClickCapture={(event) => {...}}>...</button> • イベントハンドラはDOMに直接アタッチされない • Reactがイベントを受け取りコンポーネントのイベントハンドラを 呼び出す • イベントオブジェクトはブラウザの違いを吸収したオブジェクトが渡される • 合成イベント (Synthetic Event) と呼ばれる • 合成イベントで扱えるのはReactでレンダリングした要素のみ • Reactコンポーネントに対応しないDOMノードに イベントハンドラを設定するにはDOM APIを使う 属性名は キャメルケースです 「React外リソースとの同期」 で説明します 107
  88. 合成イベント (実) DOM Document <html> <body> <div id="root"> <form> export

    function App() { const onClick = (event) => {...}; return ( <form> <button onClick={onClick}> ボタン </button> </form> ); } React ①クリック ②ネイティブ イベント ③合成 イベント <button> ReactDom.creteRoot() に渡された要素 108
  89. 演習(3-4): Todoアプリ • TodoItemコンポーネントに削除ボタンを 追加してみよう • 現段階ではタスクの削除はできないので、削除ボタンの イベントハンドラはタスクをコンソールに出力してみよう 演習3-3のProjectを開いて 左上の「Fork」で

    新しいProjectを作成しよう ... export function TodoItem(props: Props) { const onDeleteButton: xxx = () => { ... }; return ( <li> ... <button onClick={onDeleteButton}>削除</button> </li> ); } src/TodoItem.tsx onClickの上にマウスカーソルを 重ねるとイベントハンドラの 型が表示されます xxxはonClickイベント ハンドラの型 ログはDevToolsに 出力されます Projectの名称を [3-4] Todo などに変更しよう 109
  90. ReactとCSS • React自体はCSSをサポートするための最小限の機能を提供 • className属性 • HTMLのclass属性に相当 • 属性値はクラス名をスペース区切りで並べた文字列 •

    例: <div className="foo bar baz">...</div> • 属性値を組み立てるためにclsxやclassnames等のnpmパッケージが使われる • 例: <div className={clsx('foo', flag && 'bar', 'baz')}>...</div> • style属性 • HTMLのstyle属性に相当 • 属性値はJSのオブジェクトで指定する • オブジェクトのキーはCSSのプロパティ (キャメルケース) • 例: <div style={{ backgroundColor: "black" }}>...</div> 110
  91. ReactとCSSライブラリ・ツール • 主なCSSの利用方法 • CSS Modules • ローカルスコープを持つCSSファイルをJS/TSからimportして利用 • Webpackやその他のツールで幅広くサポート

    • CSS-in-JS • JS/TS内でCSSをオブジェクトリテラルやテンプレートリテラルで記述 • ランタイム系のCSS-in-JSライブラリ • 例: Emotion, styled-components, etc. • ゼロランタイム系のCSS-in-JSライブラリ • 例: Linaria, vanilla-extract, Panda CSS, StyleX • Tailwind • ユーティリティファーストのCSSフレームワーク • Tailwindが提供するCSSクラスをclassName属性で利用する 本研修ではこれらは 取り扱いません 111
  92. コンポーネントとJSX まとめ • コンポーネントはReactアプリの構成単位 • 主に関数コンポーネントとして実装する • 引数としてPropsを受け取りReactElementを返す普通の関数 • コンポーネント内にJSXでHTML風のマークアップを書ける

    • JSXはReactElementを組み立てる式に変換される • JSXの中ではJSの式を使うことができる • 制御構文もJSの式を利用する • 関数コンポーネントは仮想DOMで管理される情報 (Props, State, etc.) と関連付けられる • 関数コンポーネントはReactが管理する文脈の内側で呼び出される イメージ 112
  93. コンポーネントの状態 • コンポーネントは状態 (State) を持つことができる • 関数コンポーネントは単なる関数 • 関数自身は状態を持っていない •

    状態はReactによって管理される • 状態はReactが管理する仮想DOMに保持される • 関数コンポーネントからReactが管理する情報 (状態を含む) と やり取りをするためにHooksを使う 114
  94. Hooks • Reactによって提供されるAPI (関数) • 特に「組込Hooks」と呼ばれる • 組込Hooksを利用したユーザ定義の関数は「カスタムHooks」と呼ばれる • 関数名がuse~で始まる

    • 主な組込Hooks • 状態を扱うHooks • useState(), useReducer(), useRef() • 作用を扱うHooks • useEffect(), useLayoutEffect() • メモ化するHooks • useMemo(), useCallback() 他にも多数ありますが 本研修では扱いません 115
  95. useState() • 状態を扱う組込Hook • 使い方: [state, setState] = useState(initialValue) •

    引数は状態の初期値または初期化関数 • プリミティブ値に加えてオブジェクト (配列含む) も渡せる • 戻り値は2要素の配列 • 第1要素: 状態の現在の値 • 第2要素: 状態を更新する関数 • 引数は「新しい値」または「現在の値を受け取って新しい値を返す関数」 • 状態の更新はキューイングされる (状態は直接更新されない) • 状態が実際に更新されると再レンダリングが発生する • オブジェクトや配列はイミュータブルとして扱うこと 116
  96. 例: Counterコンポーネント import React from "react"; export function Counter() {

    const [count, setCount] = React.useState(0); const inc = () => { setCount(count + 1); }; return ( <div> <span>{count}</span> <button onClick={inc}>+</button> </div> ); } src/Counter.tsx 117
  97. Counterコンポーネントの動作 (1) Counter { } export function Counter() { const

    [count, setCount] = React.useState(0); const inc = () => { setCount(count + 1); }; return ( <div> <span>{count}</span> <button onClick={inc}>+</button> </div> ); } React アプリ ① 実行 (初回レンダリング) Props 仮想DOM 関数コンポーネント 118
  98. Counterコンポーネントの動作 (2) Counter { } export function Counter() { const

    [count, setCount] = React.useState(0); const inc = () => { setCount(count + 1); }; return ( <div> <span>{count}</span> <button onClick={inc}>+</button> </div> ); } ② Stateを作成 React 0 State アプリ 関数コンポーネント Props 仮想DOM 119
  99. Counterコンポーネントの動作 (3) Counter { } export function Counter() { const

    [count, setCount] = React.useState(0); const inc = () => { setCount(count + 1); }; return ( <div> <span>{count}</span> <button onClick={inc}>+</button> </div> ); } ③ 0とsetterが返る React 0 + 0 アプリ 関数コンポーネント Props State ④ DOM更新 コミットフェーズ 仮想DOM 120
  100. Counterコンポーネントの動作 (4) Counter { } export function Counter() { const

    [count, setCount] = React.useState(0); const inc = () => { setCount(count + 1); }; return ( <div> <span>{count}</span> <button onClick={inc}>+</button> </div> ); } ⑥ Stateを1に更新 するよう要求する React 0 + 0 ⑤ ボタンクリック 更新要求はReactにより キューイングされる アプリ 関数コンポーネント Props State setCount()からreturn した時点では状態はまだ 更新されていない 画面 仮想DOM 121
  101. Counterコンポーネントの動作 (5) Counter { } export function Counter() { const

    [count, setCount] = React.useState(0); const inc = () => { setCount(count + 1); }; return ( <div> <span>{count}</span> <button onClick={inc}>+</button> </div> ); } ⑦ Stateを更新する React 0→1 + 0 アプリ 関数コンポーネント Props State 画面 仮想DOM 122
  102. Counterコンポーネントの動作 (6) Counter { } export function Counter() { const

    [count, setCount] = React.useState(0); const inc = () => { setCount(count + 1); }; return ( <div> <span>{count}</span> <button onClick={inc}>+</button> </div> ); } React 1 + 0 ⑧ 実行 (再レンダリング) アプリ 関数コンポーネント Props State 画面 仮想DOM 123
  103. Counterコンポーネントの動作 (7) Counter { } export function Counter() { const

    [count, setCount] = React.useState(0); const inc = () => { setCount(count + 1); }; return ( <div> <span>{count}</span> <button onClick={inc}>+</button> </div> ); } ⑨ 1とsetterが返る React 1 + 1 画面 Stateは作成済みなので 初期値は使われない アプリ 関数コンポーネント 新しいイベント ハンドラが登録される Props State ⑩ DOM更新 コミットフェーズ 仮想DOM 124
  104. Counterコンポーネントの動作 (まとめ) Counter export function Counter() { const [count, setCount]

    = useState(0); return <div>...{count} ...</div>; } React 0 アプリ 関数コンポーネント const inc = () => { setCount(count + 1); }; イベントハンドラ クリック Counter export function Counter() { const [count, setCount] = useState(0); return <div>...{count} ...</div>; } 0→1 const inc = () => { setCount(count + 1); }; クリック Counter export function Counter() { const [count, setCount] = useState(0); return <div>...{count} ...</div>; } 1→2 const inc = () => { setCount(count + 1); }; Stateを1に更新要求 Stateを2に更新要求 count: 0 count: 0 count: 0 count: 1 count: 0 count: 2 再レンダリング 再レンダリング 初回 レンダリング State 仮想DOM 125
  105. 再レンダリングと宣言的UI • 状態が更新されるとコンポーネントは再レンダリングされる • 「リアクティブ (反応的)」とも呼ばれる • 状態が変更されたコンポーネントの子孫コンポーネントも 再レンダリングされる •

    再レンダリングはコンポーネントを再実行する • 状態が変わるたびにコンポーネントツリーが再実行される • 最初のレンダリング (新規表示) と再レンダリング (更新) を 区別する必要が少ない • 現在の状態をどう画面に反映するかを考えるだけ → 宣言的UI • UI = f(State) 126
  106. 演習(4-1): Counterアプリ • Stackblitzの新しいProjectでCounterアプリを作ってみよう • 「+」ボタンに加えて「-」「Reset」ボタンを追加しよう • AppコンポーネントにCounterコンポーネントを複数置いて それぞれの状態が独立していることを確認してみよう •

    「+」ボタンのイベントハンドラにsetTimeout()を加えて ステータスの更新を遅延してみよう • 例: setTimeout(() => { setCount(count + 1) }, 2000) • タイムアウトする前にボタンを連打した場合の挙動を確認してみよう Projectの名称を [4-1] Counter などに変更しよう 127
  107. 状態とクロージャ • JavaScriptの関数はクロージャ • イベントハンドラもクロージャ • クロージャは外側の変数をキャプチャする • 例: イベントハンドラはuseState()の戻り値を代入した変数を

    キャプチャする • 古い状態の変数をキャプチャしたイベントハンドラ (クロージャ) が動き続けることがある Stale Closure と呼ばれます 128
  108. JSのクロージャ function f(m: number) { return (n: number) => m

    + n; } const add1 = f(1); add1(2); // 3 const add2 = f(2); add2(2); // 4 add1 === add2 // false 戻り値の関数はクロージャ (外側の変数をキャプチャ) ソース上では同じ関数だが 実行時は異なる関数オブジェクト 関数コンポーネント内のイベントハンドラも同様 129
  109. 関数コンポーネントとクロージャ import React from "react"; export function Counter() { const

    [count, setCount] = React.useState(0); const inc = () => { setTimeout(() => setCount(count + 1), 5000); }; return ( <div> <span>{count}</span> <button onClick={inc}>+</button> </div> ); } src/Counter.tsx イベントハンドラもsetTimeout()に渡す コールバックもクロージャ (外側の変数をキャプチャ) 関数コンポーネントが実行されるたびに 新しいイベントハンドラが設定される 130
  110. 古いクロージャによる更新 (1) Counter export function Counter() { const [count, setCount]

    = useState(0); return <div>...{count} ...</div>; } React 0 アプリ 関数コンポーネント const inc = () => { setTimeout(() => { setCount(count + 1); }, 2000); }; イベントハンドラ クリック const inc = () => { setTimeout(() => { setCount(count + 1); }, 2000); }; クリック count: 0 count: 0 count: 0 同じイベントハンドラ (同じクロージャ) 初回 レンダリング State 仮想DOM タイムアウトする前に 複数回クリック 131
  111. 古いクロージャによる更新 (2) const inc = () => { setTimeout(() =>

    { setCount(count + 1); }, 2000); }; イベントハンドラ Counter export function Counter() { const [count, setCount] = useState(0); return <div>...{count} ...</div>; } 0→1 const inc = () => { setTimeout(() => { setCount(count + 1); }, 2000); }; Stateを1に更新要求 count: 0 count: 0 count: 1 タイムアウト 最初のクリックによる タイマがタイムアウト 再レンダリング React アプリ 関数コンポーネント 新しいクロージャ (上のinc()とは異なる) 仮想DOM 132
  112. 古いクロージャによる更新 (3) const inc = () => { setTimeout(() =>

    { setCount(count + 1); }, 2000); }; イベントハンドラ Counter export function Counter() { const [count, setCount] = useState(0); return <div>...{count} ...</div>; } 1→1 const inc = () => { setTimeout(() => { setCount(count + 1); }, 2000); }; Stateを1に更新要求 count: 0 count: 0 count: 1 タイムアウトで 呼ばれるのは 古いクロージャ 状態が変化しない場合、 再レンダリングは スキップされることが あります タイムアウト 再レンダリング React アプリ 関数コンポーネント 次のクリックによる タイマがタイムアウト 仮想DOM 133
  113. 更新関数による更新 (1) Counter export function Counter() { const [count, setCount]

    = useState(0); return <div>...{count} ...</div>; } React 0 アプリ 関数コンポーネント const inc = () => { setTimeout(() => { setCount(v => v + 1); }, 2000); }; イベントハンドラ const inc = () => { setTimeout(() => { setCount(v => v + 1); }, 2000); }; count: 0 初回 レンダリング State タイムアウトする前に 複数回クリック クリック クリック 仮想DOM 同じイベントハンドラ (同じクロージャ) 135
  114. 更新関数による更新 (2) const inc = () => { setTimeout(() =>

    { setCount(v => v + 1); }, 2000); }; イベントハンドラ Counter export function Counter() { const [count, setCount] = useState(0); return <div>...{count} ...</div>; } 0→1 const inc = () => { setTimeout(() => { setCount(v => v + 1); }, 2000); }; count: 1 再レンダリング Stateを「v => v + 1」の関数で更新要求 「v => v + 1」 関数を実行 React アプリ 関数コンポーネント 新しいクロージャ (上のinc()とは異なる) 最初のクリックによる タイマがタイムアウト タイムアウト 仮想DOM 136
  115. 更新関数による更新 (3) const inc = () => { setTimeout(() =>

    { setCount(v => v + 1); }, 2000); }; イベントハンドラ Counter export Counter() { const [count, setCount] = useState(0); return <div>...{count} ...</div>; } 1→2 再レンダリング const inc = () => { setTimeout(() => { setCount(v => v + 1); }, 2000); }; Stateを「v => v + 1」の関数で更新要求 count: 2 「v => v + 1」 関数を実行 React アプリ タイムアウトで 呼ばれるのは 古いクロージャ 関数コンポーネント 次のクリックによる タイマがタイムアウト タイムアウト 仮想DOM 137
  116. 例: Userコンポーネント import React from "react"; export function User() {

    const [userName, setUserName] = React.useState(""); const [birthday, setBirthday] = React.useState(""); const handleChangeUserName: React.ChangeEventHandler<HTMLInputElement> = (event) => { setUserName(event.currentTarget.value); }; const handleChangeBirthday: React.ChangeEventHandler<HTMLInputElement> = (event) => { setBirthday(event.currentTarget.value); }; const handleClickReset: React.MouseEventHandler<HTMLButtonElement> = () => { setUserName(""); setBirthday(""); } return ( <div> <label>名前<input type="text" value={userName} onChange={handleChangeUserName} /></label> <label>生年月日<input type="date" value={birthday} onChange={handleChangeBirthday} /></label> <button onClick={handleClickReset}>リセット</button> </div> ); } src/User.tsx 複数のuseState() 複数のstateを更新 140
  117. Userコンポーネントの動作 (1) User { } import React from "react"; export

    function User() { const [userName, setUserName] = React.useState(""); const [birthday, setBirthday] = React.useState(""); const handleChangeUserName = (event) => { setUserName(event.currentTarget.value); }; const handleChangeBirthday = (event) => { setAgreement(event.currentTarget.value); }; return ( <div> <input type="text" value={userName} onChange={handleChangeUserName} /> <input type="date" value={birthday} onChange={handleChangeBirthday} /> </div> ); } React アプリ ① 実行 (初回レンダリング) Props 関数コンポーネント 仮想DOM 141
  118. Userコンポーネントの動作 (2) User { } ② Stateを作成 React "" State

    アプリ 関数コンポーネント Props import React from "react"; export function User() { const [userName, setUserName] = React.useState(""); const [birthday, setBirthday] = React.useState(""); const handleChangeUserName = (event) => { setUserName(event.currentTarget.value); }; const handleChangeBirthday = (event) => { setAgreement(event.currentTarget.value); }; return ( <div> <input type="text" value={userName} onChange={handleChangeUserName} /> <input type="date" value={birthday} onChange={handleChangeBirthday} /> </div> ); } 仮想DOM 142
  119. Userコンポーネントの動作 (3) Counter { } ③ Stateを作成 React "" State

    アプリ 関数コンポーネント Props import React from "react"; export function User() { const [userName, setUserName] = React.useState(""); const [birthday, setBirthday] = React.useState(""); const handleChangeUserName = (event) => { setUserName(event.currentTarget.value); }; const handleChangeBirthday = (event) => { setAgreement(event.currentTarget.value); }; return ( <div> <input type="text" value={userName} onChange={handleChangeUserName} /> <input type="date" value={birthday} onChange={handleChangeBirthday} /> </div> ); } "" State useState()が 呼び出される毎に state情報が作成される 仮想DOM 143
  120. Hooksのルール • Reactは組込Hookが呼び出されるたび、仮想DOMに 組込Hookごとの情報を追加する • 単純な連結リストとして管理される • 関数コンポーネントは常に同じ順番で同じ数のHooksを 呼び出さなくてはならない •

    組込HooksだけではなくカスタムHooksも同様 • 関数コンポーネントのトップレベルでのみHooksを呼び出す • if文の中や条件式の中、繰り返しの中からHooksを呼び出さない • eslint-plugin-react-hooksでチェックする useState()に限らず Hook全般のルールです 144
  121. 間違ったHooksの使い方 (1) Counter { } React false State アプリ 関数コンポーネント

    Props import React from "react"; export function User() { const [state1, setState1] = React.useState(false); if (state1) { const [state2, setState2] = React.useState(0); } const [state3, setState3] = React.useState(""); return ( ... ); } "" State 仮想DOM 145
  122. 間違ったHooksの使い方 (2) Counter { } React true State アプリ 関数コンポーネント

    Props import React from "react"; export function User() { const [state1, setState1] = React.useState(false); if (state1) { const [state2, setState2] = React.useState(0); } const [state3, setState3] = React.useState(""); return ( ... ); } "" State state1がtrueになって 再レンダリングが 発生すると… Error Rendered more hooks than during the previous render. 仮想DOM 146
  123. 例: useState()の使い方 import React from "react"; export function User() {

    const [firstName, setFirstName] = useState(""); const [lastName, setLastName] = useState(""); const [fullName, setFullName] = useState(""); const handleChangeFirstName = (event) => { setFirstName(event.target.value); setFullName(`${event.target.value} ${lastName}`); }; const handleChangeLastName = (event) => { setLastName(event.target.value); setFullName(`${firstName} ${event.target.value}`); }; return ( ... ); } import React from "react"; export function User() { const [user, setUser] = useState({ firstName: "", lastName: "", }); const fullName = `${user.firstName} ${user.lastName}`; const handleChangeFirstName = (event) => { setUser((user) => ({ ...user, firstName: event.target.value })); }; const handleChangeLastName = (event) => { setUser((user) => ({ ...user, lastName: event.target.value })); }; return ( ... ); } 関連のある stateはまとめる 導出値にstateは 使わない イミュータブルな 更新 よくない例 改善例 148
  124. イミュータブルな更新 • オブジェクト (配列を含む) は直接更新しない • プロパティや要素の書き換え、追加・削除はしない • 変更後のプロパティや要素を持つ新しいオブジェクトを作る •

    イミュータブルなマナーに従う • オブジェクトのイミュータブルな更新 • 例: { ...oldObject, foo: newFoo, bar: newBar } • 配列のイミュータブルな更新 • 要素の追加: [...oldArray, newItem] • 要素の置換: Array.prototype.map() • または: Array.prototype.with() // ES2023 149
  125. フォームと制御コンポーネント • Reactで入力要素等を扱う方法 • 制御コンポーネント (Controlled Component) • React-wayな方法 (宣言的)

    で入力要素等を扱う • 非制御コンポーネント (Uncontrolled Component) • React-wayではない方法 (命令的) で入力要素等を扱う • 制御コンポーネント • 入力要素等の状態はReactコンポーネントが管理する • useState()/useReducer()を使う • Reactコンポーネントの状態更新による再レンダリングで 入力要素等が更新される • Reactコンポーネントの状態が更新されなければ入力要素等は更新されない 「パフォーマンスとメモ化」 で扱います 150
  126. 入力要素等と制御コンポーネント import React from "react"; export function ReadonlyText() { const

    text = "Foo"; const handleTextChange: React.ChangeEventHandler<HTMLInputElement> = () => { }; return <input type="text" value={text} onChange={handleTextChange} />; } export function WritableText() { const [text, setText] = React.useState("Foo"); const handleTextChange: React.ChangeEventHandler<HTMLInputElement> = (event) => { setText(event.currentTarget.value); }; return <input type="text" value={text} onChange={handleTextChange} />; } テキストフィールドに 入力しても反映されない テキストフィールドに 入力すると反映される 151
  127. 入力要素等と制御コンポーネント Text export function Text() { const [text, setText] =

    useState(""); const handleChangeText = () => {...}; return <input value={text} ... />; } React "" アプリ 関数コンポーネント 入力 Counter export function Text() { const [text, setText] = useState(""); const handleChangeText = () => {...}; return <input value={text} ... />; } ""→"a" Counter export function Text() { const [text, setText] = useState(""); const handleChangeText = () => {...}; return <input value={text} ... />; } "a"→"ab" Stateを"a"に更新要求 Stateを"ab"に更新要求 再レンダリング 再レンダリング 初回 レンダリング State 'a' 'b' DOM更新 入力 'b' DOM更新 a ab 画面 仮想DOM 152
  128. フォームとイベントハンドラ (1) • フォーム • onSubmitイベントハンドラでフォームサブミット時のイベントを扱う • イベントハンドラではevent.preventDefault()を呼び出す • 例:

    <form onSumbit={handleSubmit} >...</form> • ボタン • onClickイベントハンドラでボタン押下時のイベントを扱う • disabled属性 (boolean) で有効/無効を指定する • 例: <button onClick={handleClick} disabled={flag}>ボタン</button> onSubmitを使わない 方法も増えています 153
  129. フォームとイベントハンドラ (2) • テキストフィールド • value属性 (string) でテキストを指定 • onChangeイベントハンドラでテキスト入力時のイベントを扱う

    • event.currentTarget.value (string)で入力値を取得 • 例: <input type="text" value={text} onChange={handleChange} /> • テキストエリア • value属性 (string) でテキストを指定 • onChangeイベントハンドラでテキスト入力時のイベントを扱う • event.currentTarget.value (string)で入力値を取得 • 例: <textarea value={text} onChange={handleChange}></textarea> 通常のHTMLとは 異なります 154
  130. フォームとイベントハンドラ (3) • チェックボックス • checked属性 (boolean) で選択・未選択を指定 • onChangeイベントハンドラで選択状態変更時のイベントを扱う

    • event.currentTarget.checked (boolean)で選択状態を取得 • 例: <input type="checkbox" name="option" value={1} checked={checked} onChange={handleChange} /> 155
  131. フォームとイベントハンドラ (4) • ラジオボタン • checked属性 (boolean) で選択・未選択を設定 • onChangeイベントハンドラで選択状態変更時のイベントを扱う

    • event.currentTarget.valueに選択されたラジオボタンのvalue属性値が入る • 例: const colors = [{ label: "赤", value: "red" }, { label: "緑", value: "green" }, { label: "青", value: "blue" }]; function ColorForm() { const [selected, setSelected] = React.useState(""); const handleChange: React.ChangeEventHandler<HTMLInputElement> = (event) => setSelected(event.currentTarget.value); return ( <form> {colors.map((color) => ( <label>{color.label} <input type="radio" name="color" value={color.value} checked={color.value === selected} onChange={handleChange} /> </label> ))} </form> ); } 156
  132. フォームとイベントハンドラ (5) • 選択リスト • <select>要素のvalue属性 (stringまたはstring[]) で 選択中の<option>要素を指定 •

    onChangeイベントハンドラで選択状態変更時のイベントを扱う • event.currentTarget.valueに選択された<option>要素のvalue属性値が入る • 例: const colors = [{ label: "赤", value: "red" }, { label: "緑", value: "green" }, { label: "青", value: "blue" }]; function ColorForm() { const [selected, setSelected] = React.useState(""); const handleChange: React.ChangeEventHandler<HTMLSelectElement> = (event) => setSelected(event.currentTarget.value); return ( <select value={selected} onChange={handleChange}> {colors.map((color) => ( <option value={color.value}>{color.label}</option> ))} </select> ); } 通常のHTMLとは 異なります 157
  133. 演習(4-3): Todoアプリ • テキストフィールドと「追加」ボタンで新しい Todoを追加できるようにしよう • テキストフィールドが未入力なら「追加」ボタンを 押せないようにしよう • テキストフィールド内で「Enter」キーを押すだけでTodoを

    追加できるようにしよう • Todoの完了/未完了を変更できるようにしよう • Todoを削除できるようにしよう • 完了/未完了のTodoをフィルタリングできるようにしよう • 全て/完了/未完了の選択リストを追加してみよう 演習3-4のProjectを開いて 左上の「Fork」で 新しいProjectを作成しよう Projectの名称を [4-3] Todo などに変更しよう 158
  134. useReducer() • useState()の課題 • Stateをどのように更新するかがイベントハンドラに散らばりやすい • useReducer() • Stateの更新処理を「Reducer」関数に集約できる組込Hook •

    ActionとReducerを使ってStateを更新する • Action • Stateをどのように更新するか指示するもの (通常はオブジェクト) • Reducer • 現在のStateとActionを受け取って新しいStateを返す関数 • Array.prototype.reduce()に渡す関数と同様 159
  135. useReducer() • 使い方: [state, dispatch] = useReducer(reducer, initialState) • 引数でReducerとStateの初期値を渡す

    • Reducer • 「現在のState」と「Action」を引数で受け取り「新しいState」を返す関数 • (state, action) => state • 戻り値は配列 • 1番目の要素は現在のState • 2番目の要素はStateの更新を要求するための関数 • 引数にActionを渡して呼び出すと現在のステートと共にReducerに渡され Stateが更新される • Stateの更新はuseState()と同様にキューイングされる 160
  136. 例: useRecuder()版のCounter // Action types type Inc = { type:

    "Inc"; step: number; }; type Dec = { type: "Dec"; step: number; }; type Reset = { type: "Reset"; value: number; }; type Action = Inc | Dec | Reset; // Action creators const inc: (step?: number) => Inc = (step = 1) => ({ type: "Inc", step }); const dec: (step?: number) => Dec = (step = 1) => ({ type: "Dec", step }); const reset: (value?: number) => Reset = (value = 0) => ({ type: "Reset", value }); const reducer = (state: number, action: Action): number => { switch (action.type) { case "Inc": return state + action.step; case "Dec": return state - action.step; case "Reset": return action.value; } }; export Counter() { const [count, dispatch] = React.useReducer(reducer, 0); return ( <div> <span>{count}</span> <button onClick={() => dispatch(inc())}>+</button> <button onClick={() => dispatch(dec())}>-</button> <button onClick={() => dispatch(reset())}>Reset</button> </div> ); } Action Reducer Counterコンポーネント 161
  137. 参考: Array.reduce() [ inc(), inc(), dec(), ] .reduce(reducer, 0 );

    (state, action) => 1 (state, action) => 2 (state, action) => 1 Reducer const reducer = (state: number, action: Action): number => { switch (action.type) { case "Inc": return state + action.step; case "Dec": return state - action.step; case "Reset": return action.value; } }; 162
  138. useReducer()の考え方 [ inc(), inc(), dec() ] .reduce(reducer, 0 ); (state,

    action) => 1 (state, action) => 2 (state, action) => 1 固定長の配列ではなく 将来発生するActionの 時系列と考える Reducerが返した 個々の値の時系列を Stateと考える Reducer const reducer = (state: number, action: Action): number => { switch (action.type) { case "Inc": return state + action.step; case "Dec": return state - action.step; case "Reset": return action.value; } }; 163
  139. 演習(4-4): Todoアプリ (Reducer版) • TodoアプリのuseState()をuseReducer()に 置き換えてみよう • useState()とuseReducer()の使い分けについて考えてみよう • useState()が向いているStete

    • useReducer()が向いているStete 演習4-3のProjectを開いて 左上の「Fork」で 新しいProjectを作成しよう Projectの名称を [4-4] Todo などに変更しよう 164
  140. useRef() • 再レンダリングを引き起こさない状態を扱う組込Hook • 使い方: ref = useRef(initialValue) • 引数は戻り値となるRefオブジェクトのcurrentプロパティの初期値

    • 戻り値はcurrentプロパティを持つRefオブジェクト • レンダリング毎に常に同じオブジェクトが返される • Refオブジェクト • currentプロパティに任意の値を設定できる • useState()/useReducer()が返す状態と異なり直接更新することができる • イミュータブルなマナーに従う必要はない • 更新はキューイングされない 165
  141. useRef()の例 (インスタンス変数的) import React from "react"; export function RefCounter() =>

    { const [stateCount, setStateCount] = React.useState(0); const refCount = React.useRef(0); const handleClickStateCount = () => { setStateCount((v) => v + 1); }; const handleClickRefCount = () => { refCount.current++; }; // →へ続く src/RefCounter.tsx return ( <div> <div> <div>State Counter</div> <div> <span>{stateCount}</span> <button onClick={handleClickStateCount}>+</button> </div> </div> <div> <div>Ref Counter</div> <div> <span>{refCount.current}</span> <button onClick={handleClickRefCount}>+</button> </div> </div> </div> ); } 167
  142. useRef()によるDOM要素との連携 • ホストコンポーネントに対応するDOM要素の参照を取得できる • ホストコンポーネントのref属性にuseRef()の戻り値を渡す • 例: const inputRef =

    useRef<HTMLInputElement>(null!); <input ref={inputRef} ... /> • コミットフェーズでinputRef.currentにDOM要素が設定される • 最初にコンポーネントが実行される時点では未設定 • イベントハンドラからDOM要素にアクセスすることができる • 関数コンポーネント本体からはDOM要素にアクセスすべきではない 168
  143. useRef()の例 (DOM要素の取得) import React from "react"; export function RefCounter() {

    const buttonRef = React.useRef(null!); const handleClick = () => { const buttonElement = buttonRef.current; ... }; return ( <div> <button ref={buttonRef} onClick={handleClick}>...</button> </div> ); } src/RefCounter.tsx イベントハンドラが呼び出された 時点ではcurrentプロパティに DOM要素が設定されている 169
  144. forwardRef() • 子コンポーネントはPropsで「ref」を受け取ることができない • refという名前以外でなら受け取ることができる • 例: inputRef, innerRef, etc.

    • 子コンポーネントがrefという名前でRefオブジェクトを 受け取れるようにするにはforwardRef()を使う • <Input>や<Button>等、プロジェクト固有のスタイルを与えた 基本的なコンポーネントでよく使用する • 使い方: Component = forwardRef((props, ref) => {...}) • 引数はrefを転送する関数 • 引数にPropsとRefを受け取りReactElement等を返す • 戻り値はrefを転送できる関数コンポーネント 171
  145. forwardRef() import React from "react"; type Props = React.ButtonHTMLAttributes<HTMLButtonElement>; const

    Button = React.forwardRef<HTMLButtonElement, Props>((props, ref) => { const { children, ...rest } = props; return ( <button ref={ref} {...rest}> {children} </button> ); }); src/Button.tsx import React from "react"; export default function App() { const ref = React.useRef<HTMLButtonElement>(null!); return ( <Button ref={buttonRef} onClick={() => console.log("clicked")}>Refを渡せる喜び</button> ); } src/App.tsx 172
  146. 状態とスコープ • useState()/useReducer()/useRef()による状態は コンポーネント固有 • コンポーネントの「ローカルステート」と呼ばれる • 空間的なスコープ (可視範囲) •

    useState()等を呼び出したコンポーネントのみが直接参照できる • Propsを通じて子コンポーネントに状態を渡すことができる • 該当コンポーネントとその子孫が空間的スコープ • 時間的なスコープ (存続期間、ライフタイム) • useState()等を呼び出したコンポーネントが表示されている (マウントされている) 間のみ状態が存続する • 該当コンポーネントが親コンポーネントによってレンダリング されている期間が時間的スコープ 173
  147. 状態のスコープを広げる • より広範囲のコンポーネントでも状態を共有したい場合 • 状態を親 (祖先) に移動する (状態のリフトアップ) • useState()等の呼び出しを祖先コンポーネントで行う

    • より空間的に広い範囲のコンポーネントに状態を渡せる • より時間的に長い存続期間を持つことができる • デメリット • 子孫に状態をPropsで受け渡す必要がある • Propsのバケツリレー • 対策: Contextを導入する • Propsのバケツリレーを回避できる • 注意深く使わないと再レンダリングが増える 本研修ではContextは 扱いません 174
  148. 状態と再レンダリング まとめ • コンポーネントは状態 (State) を持つ • 関数コンポーネントそのものが状態を持つわけではない • Reactが管理する仮想DOMに状態が保持される

    • 組込HooksのuseState(), useReducer()で状態にアクセス • 状態が更新されると再レンダリングが発生する • 関数コンポーネントは再実行される • 最初の表示と同様に実行されるので「更新」を意識しない (宣言的UI) • 状態としてオブジェクト (配列を含む) を使っている場合 • 状態の更新はミュータブルなマナーに従う • useState()では「古くなったクロージャ」に注意 • 「現在の値に基づく更新」は更新関数を利用する • useRef()で再レンダリングを伴わない状態を扱うことができる 176
  149. Reactの宣言的UIとリソース • Reactの宣言的UIで扱えるのはDOMの一部だけ • ルートとなるDOM要素とその子孫の要素、属性、テキスト • Webアプリで扱う範囲はもっと広い • DOM •

    Reactで扱える範囲の外側 • Document, html要素, head要素, body要素, etc. • DOM APIのメソッドを利用する機能 • フォーカス, スクロール, etc. • DOM以外のブラウザが提供する機能 • Fetch, WebSocket, Local/SessionStorage, History, Workers, etc • ブラウザの外 • Web上のサービス (Web API) 178
  150. Reactとリソース コンポーネント リソース全体 ブラウザ DOM Reactで扱える範囲 要素, 属性, テキスト Document,

    フォーカス, スクロール, History, etc. Fetch, WebSocket, Local/SessionStorage, History, etc. Reactが反映 ? Web上のサービス 179
  151. コンポーネントと副作用 • コンポーネントの主目的はDOM要素のレンダリング • それ以外のリソースを扱うことは「副作用」 • コンポーネント自体は副作用を持つべきではない • コンポーネントはレンダーフェーズで実行される •

    レンダーフェーズは途中で破棄されて再実行されることもある • コンポーネントが何度実行されるかはReactのスケジューリング次第 • コンポーネントは「べき等」であるべき • 繰り返し実行されても不都合がないこと • イベントハンドラのようにレンダーフェーズで実行されないコードは副 作用を持っても構わない 180
  152. 宣言的UIとリソース • React管理外のリソースも宣言的UIのマナーに従って扱う • メンタルモデル • Reactのレンダリングとリソースを「同期」する • 例: •

    時計コンポーネントはタイマと同期する • 時計コンポーネントが表示されている間はタイマで監視された状態にある • チャットコンポーネントはチャットサーバと同期する • チャットコンポーネントが表示されている間はチャットサーバからの 通知を受け取れる (サブスクリプションしている) 状態にある 181
  153. useEffect() • 作用を扱うための組込Hook • コンポーネント視点では「副作用」だがuseEffect()視点では「作用」 • useEffect()の主目的のため「副」作用ではないという扱い • 使い方: useEffect(effectFunction,

    deps) • 第1引数は「作用」をセットアップする関数 • 「作用」をクリーンナップする関数を返す (省略可) • 第2引数は作用が依存する値の配列 (省略可) • 配列の各要素は前回のレンダリング時の対応する要素とObject.is()で比較される • オブジェクト (配列や関数を含む) は同一性に注意 「パフォーマンスとメモ化」 で説明します 182
  154. セットアップ/クリーンナップ関数 • セットアップ関数 • リソースと同期した状態を開始する関数 • 例 • タイマを設定する、イベントリスナーを登録する、ネットワークに接続する •

    引数: なし • 戻り値: クリーンナップ関数 • クリーンナップ関数 • リソースと同期した状態を終了する関数 • 例 • タイマを解除する、イベントリスナーを削除する、ネットワークを切断する • 引数: なし • 戻り値: なし 183
  155. セットアップ/クリーンナップ関数の例 useEffect(() => { // setup const timerId = setTimeout(()

    => { ... }, 5000); return () => clearTimeout(timerId); // cleanup }); useEffect(() => { // setup const mouseMoveListener = () => { ... }; document.addEventListener("mousemove", mouseMoveListener); return () =>document.removeEventListener("mousemove", mouseMoveListener); // cleanup }); useEffect(() => { // setup const controller = new AbortController(); const signal = controller.signal; document.addEventListener("mousemove", () => { ... }, { signal }); document.addEventListener("wheel", () => { ... }, { signal }); return () =>controller.abort(); // cleanup }); コンポーネントを 「タイムアウト時間が 設定されている状態」 と同期する コンポーネントを 「mousemoveイベントを 監視している状態」 と同期する AbortControllerを使うと クリーンナップ関数が 簡潔に書ける 184
  156. Clockコンポーネント export function Clock() { const [date, setDate] = React.useState(new

    Date()); const formatter = new Intl.DateTimeFormat("ja-JP", { hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: true }); const time = formatter.format(date); React.useEffect(() => { // setup const timerId = setTimeout(() => { setDate(new Date()); }, 5000); return () => { // cleanup clearTimeout(timerId); }; }); return ( <div> <div><span>{time}</span></div> </div> ); } src/Clock.tsx 185
  157. 依存配列 (deps) • useEffect()の第2引数 • リソースが何と同期するかを指定する • 依存配列を省略 • リソースは毎回のレンダリングと同期する

    • 同期の頻度: 多 • 1要素以上の配列 • リソースは配列の要素である変数 (の値) と同期する • 同期の頻度: 中 • 空配列 • リソースはコンポーネント自体と同期する • 同期の頻度: 少 論理的にはこれで 正しく動作すべき 最適化 187
  158. 依存配列 (deps) とリソースの同期 レンダー コミット レンダー コミット レンダー コミット レンダー

    コミット { a: 0, b: 0 } DOM更新 { a: 0, b: 1 } DOM更新 { a: 1, b: 0 } DOM更新 { a: 1, b: 1 } DOM更新 レンダー コミット DOM更新 レンダ リングと 同期 した状態 setup cleanup レンダ リングと 同期 した状態 setup cleanup レンダ リングと 同期 した状態 setup cleanup レンダ リングと 同期 した状態 setup cleanup 親コンポーネントから削除 0:0 function Foo({a, b}) { useEffect(() => {...}); return <p>{a}:{b}</p>; } コンポーネント実行 0:1 1:0 1:1 { a: 0 } と同期 した状態 setup cleanup { a: 1 } と同期 した状態 setup cleanup コンポー ネント と同期 した状態 setup cleanup function Bar({a, b}) { useEffect(() => {...}, [a]); return <p>{a}:{b}</p>; } function Baz({a, b}) { useEffect(() => {...}, []); return <p>{a}:{b}</p>; } Props コンポーネント実行 コンポーネント実行 コンポーネント実行 188
  159. 依存配列とクロージャ • セットアップ/クリーンナップ関数の内部から 外側のコンポーネントで定義された変数を参照できる • セットアップ/クリーンナップ関数はクロージャ • セットアップ/クリーンナップ関数から参照している変数を 依存配列に指定する •

    変数の値が変わった場合はリソースと同期し直す • 例外: 変化しない (イミュータブルな) 変数 • 例: useState()が返すsetter, useReducer()が返すdispatch, useRef()が返すRef • eslint-plugin-react-hooksでチェックする • 将来的にはReact Compilerによって自動的に補われる予定 189
  160. 演習(5-3): Clockアプリ • setTimeout()の代わりにsetInterval()を使ってみよう • 依存配列を適切に変更しよう • 時刻を更新するインターバルをテキストフィールドで 設定できるようにしてみよう •

    依存配列を適切に変更しよう • Clockコンポーネントの先頭やセットアップ/クリーンナップ 関数にログ出力を入れて動作を確認してみよう • リロードした直後の動作を確認してみよう • ブラウザのリロードボタンではなくブラウザ内ブラウザのリロードボタンを使用 演習5-2のProjectを開いて 左上の「Fork」で 新しいProjectを作成しよう Projectの名称を [5-3] Clock などに変更しよう 191
  161. Strict Mode • 不正な副作用のあるコンポーネントを早期検出するための機能 • <React.StrictMode>...</React.StrictMode> • StackBlitzの「React TypeScript」でも適用されている •

    src/main.tsx • 開発モードでは: • レンダーフェーズが2回ずつ実行される • コンポーネントが最初にレンダリングされる際はセットアップ関数も 2回実行される • セットアップ関数 → クリーンナップ関数 → セットアップ関数 192
  162. Strict Mode無効時の動作 レンダーフェーズ コミットフェーズ Effectセットアップ レンダーフェーズ コミットフェーズ コミットフェーズ Effectクリーンナップ コンポーネントが最初に

    レンダリングされるとき 再レンダリング されるとき Effectクリーンナップ Effectセットアップ コンポーネントが 削除されたとき Paint Paint Paint 193
  163. Strict Mode有効時の動作 レンダーフェーズ レンダーフェーズ コミットフェーズ Effectセットアップ Effectクリーンナップ Effectセットアップ レンダーフェーズ レンダーフェーズ

    コミットフェーズ コミットフェーズ Effectクリーンナップ コンポーネントが最初に レンダリングされるとき 再レンダリング されるとき Effectクリーンナップ Effectセットアップ コンポーネントが 削除されたとき Paint Paint Paint 194
  164. useLayoutEffect() • セットアップ/クリーンナップ関数をコミットフェーズで 「同期的」に実行する組込Hook • 使い方: useLayoutEffect(setupFunction, deps) • DOMを更新されたブラウザが画面を「ペイントする前」に

    セットアップ/クリーンナップ関数を実行する • DOM要素がペイントされる前にそのサイズや位置等を制御したい 場合に使える • useEffect()は両方の関数を「非同期」に実行する • 両関数ともブラウザが画面をペイントした後に実行される • パフォーマンスに悪影響を与える可能性があるので可能なら useEffect()を使用すべき 195
  165. useLayoutEffectの動作 (非Strict Mode) レンダーフェーズ コミットフェーズ Effectセットアップ レンダーフェーズ コミットフェーズ コミットフェーズ Effectクリーンナップ

    コンポーネントが最初に レンダリングされるとき 再レンダリング されるとき Effectクリーンナップ Effectセットアップ コンポーネントが 削除されたとき LayoutEffectセットアップ LayoutEffectクリーンナップ LayoutEffectセットアップ Paint Paint Paint LayoutEffectクリーンナップ 196
  166. useLayoutEffect()の例 import React from "react"; export function Scroll() { const

    ref = React.useRef<HTMLDivElement>(null!); React.useLayoutEffect(() => { ref.current.scrollIntoView(); }, []); return ( <div> <ul> {new Array(1000).fill(0).map((_, index) => ( <li key={index}>{index}</li> ))} </ul> <div ref={ref}></div> </div> ); } src/Scroll.tsx 実行環境によっては useEffect()との違いを 目視できません useEffect()を使うと スクロール位置が 移動する前の状態が 一瞬見える場合がある 197
  167. React管理外のリソースと作用 まとめ • コンポーネントからReact管理外のリソースを扱うことは 副作用となる • コンポーネントから直接リソースを操作してはいけない • コンポーネントを何度実行するかはReactのスケジューラ次第 •

    コンポーネントは「べき等」であるべき • React管理外のリソースはuseEffect()/useLayoutEffect()に渡す セットアップ/クリーンナップ関数でReactと「同期」する • セットアップ関数は同期した状態を開始する • クリーンナップ関数は同期した状態を終了する • useEffect()/useLayoutEffect()に渡す依存配列でリソースと 「同期」する頻度をコントロールする 198
  168. レンダーフェーズとパフォーマンス • 前提: 仮想DOMの構築は (実) DOMの構築よりも軽量 • そのため毎回コンポーネントを再実行しても影響は少ない • 現実:

    大規模な画面では仮想DOMも巨大になる • 30行×30列のテーブルがある場合 • セルコンポーネントは約1000個 • セルコンポーネントごとに10個の子コンポーネントを持つ場合 • テーブル全体は約1万個のコンポーネント • レンダーフェーズの重さが課題になり得る • Reactはレンダーフェーズを分割実行するので画面が固まることは 生じにくいが画面が更新されるまでの遅延は低減できない • コンポーネントの不要な再レンダリングを抑止したい! 201
  169. 演習(6-1): React DevTools • React Developer Toolsをインストールしよう • https://chrome.google.com/webstore/detail/react-developer- tools/fmkadmapgofadopljbjfkapdkoienihi

    • Stackblitzを開いたタブでChrome DevToolsを開き、 「Components」タブで「View Settings 」→ 「Highlight updates when components render.」を チェックしよう 203
  170. 演習(6-2): Todoアプリ • 演習(4-5)のTodoアプリを独立したタブで開いてみよう • ブラウザ内ブラウザ領域の上にあるアドレスバーの右端にある 「Open Preview in new

    tab」をクリック • Chrome DevToolsを開いてTodoアプリを操作し、 再レンダリングされる様子を見てみよう • 新しいTodoを入力するテキストフィールドに文字を入力してみよう • TodoItemの状態を変更してみよう この表示が出たときは 「Connect to Project」を クリックした上で タブを開き直してください 204
  171. 再レンダリングを抑止する • 状態のスコープを狭くする • 状態が更新される要因 (ユーザのインタラクション) ごとに コンポーネントを分割する • 例:

    テキストフィールドに1文字入力されるたびに更新される必要が あるコンポーネントはどの範囲か? 208
  172. Todoアプリの再レンダリング App TodoItem <input type="text"> <input type="checkbox"> Form TodoItem ②onChanegイベント

    ①1文字入力 ③状態を更新 ④再レンダリング <input type="checkbox"> 209
  173. 非制御コンポーネント • Reactで入力要素等を扱う方法 • 制御コンポーネント (Controlled Component) • React-wayな方法 (宣言的)

    で入力要素等を扱う • 非制御コンポーネント (Uncontrolled Component) • React-wayではない方法 (命令的) で入力要素等を扱う • 非制御コンポーネント • 入力要素等の状態をReactで管理しない • DOMの状態が「Single Source of Truth」→ 再レンダリングを抑制 • 使い方: 入力要素等にvalue/checked属性を指定しない • defaultValue/defaultChecked属性で初期値を指定できる • 初期値を表示した後はDOMの状態が「Single Source of Truth」 • イベントハンドラでRefからフォーム入力要素の状態を取得 近年はこちらが主に 使われるように なってきました 211
  174. 非制御コンポーネント import React from "react"; export function Form() { const

    inputRef = React.useRef<HTMLInputElement>(null!); const handleSubmit: React.FormEventHandler<HTMLFormElement> = (event) => { event.preventDefault(); console.log(inputRef.current.value); inputRef.current.value = ""; inputRef.current.focus(); }; return ( <form onSubmit={handleSubmit}> <input type="text" ref={inputRef} defaultValue="" /> <button type="submit">Submit</button> </form> ); } Refを通じて要素の 属性を更新できる テキストフィールドの 内容をRefから取得 212
  175. 制御コンポーネント (再掲) Text export function Text() { const [text, setText]

    = useState(""); const handleChangeText = () => {...}; return <input value={text} ... />; } React "" アプリ 関数コンポーネント 入力 Counter export function Text() { const [text, setText] = useState(""); const handleChangeText = () => {...}; return <input value={text} ... />; } ""→"a" Counter export function Text() { const [text, setText] = useState(""); const handleChangeText = () => {...}; return <input value={text} ... />; } "a"→"ab" Stateを"a"に更新要求 Stateを"ab"に更新要求 再レンダリング 再レンダリング 初回 レンダリング State 仮想DOM 'a' 'b' DOM更新 入力 'b' DOM更新 a ab 画面 213
  176. 非制御コンポーネント Text export function Text() { const ref = useRef(null!);

    ... return <input ref={ref} ... />; } React アプリ 関数コンポーネント 入力 初回 レンダリング Ref 'a' 'b' DOM更新 入力 'b' a ab 画面 再レンダリング なしに反映 onChangeイベント onChangeイベント オートコンプリートや バリデーションは可能 極力状態を更新しない 仮想DOM 214
  177. 演習(6-4): Todoアプリ • Formコンポーネントのテキストフィールドを 非制御コンポーネント化してみよう • テキストフィールドに文字を入力してもFormコンポーネントが 再レンダリングされないことを確認しよう • テキストフィールドの状態に応じて「追加」ボタンの

    有効/無効を再レンダリングなしに切り替えられるように してみよう 演習6-3のProjectを開いて 左上の「Fork」で 新しいProjectを作成しよう Projectの名称を [6-4] Todo などに変更しよう 215
  178. React.memo() • Reactコンポーネントをメモ化する • 使い方: Memoized = React.memo(Component, compare) •

    第1引数はメモ化する対象のコンポーネント • 第2引数はPropsを比較する関数 (任意) • デフォルトはPropsオブジェクトを「浅い比較」する • 各プロパティについてObject.is()で比較する • 戻り値はメモ化されたコンポーネント • 直前に実行された場合と同じPropsで呼び出された場合は引数の コンポーネントを実行しない • Reactが管理している前回の実行結果 (仮想DOM) を再利用する 217
  179. オブジェクト・関数の同一性 • React.memo()はPropsの各プロパティをObject.is()で比較する • useEffect()/useLayoutEffect()の依存配列も同様 • Object.is()はオブジェクトをStrict Equality (===) で比較する

    • 関数もオブジェクト • オブジェクトリテラルや関数式は評価されるたびに 新しいオブジェクトを作成する • 再レンダリングで関数コンポーネントが実行されるたびに 関数内に記述されたオブジェクトリテラルや関数式は 新しいオブジェクトや関数を作成する • Propsがオブジェクトや関数を含む場合はレンダリングごとに Propsが異なってしまう 220
  180. オブジェクト・関数の同一性 import React from "react"; import { Child } from

    "./Child"; const MemoizedChild = React.memo(Child); export function Foo () { const point = { x: 100, y: 200 }; const handleEvent = () => {}; return ( <MemoizedChild point={point} handleEvent={handleEvent} /> ); } import React from "react"; import { Child } from "./Child"; const MemoizedChild = React.memo(Child); export function Foo() { const point = { x: 100, y: 200 }; const handleEvent = () => {}; return ( <MemoizedChild point={point} handleEvent={handleEvent} /> ); } { x: 100, y: 200 } { x: 100, y: 200 } () => {} () => {} 等しくない MemoizedChildは再レンダリングされる レンダリング1 レンダリング2 221
  181. React.useMemo() • 任意の値をキャッシュするための組込Hook • 同一性のためだけではなく重い計算をキャッシュする用途でも使える • 使い方: cached = useMemo(calculate,

    deps) • 第1引数はキャッシュされる値を計算する関数 • 第2引数はキャッシュする値が依存する値の配列 • 配列の各要素をObject.is()で比較する • useEffect()のdepsと似ている (省略は不可) • 戻り値はキャッシュされた値 • 前回のレンダリング時とdepsの全要素が等しければ キャッシュされた値が返される • それ以外はcalculateが再実行されてその戻り値が返される 222
  182. React.useCallback() • 関数をキャッシュするための組込Hook • 使い方: cached = useCallback(fn, deps) •

    useMemo(() => fn, deps)と同等 • 第1引数はキャッシュ対象の関数 • 第2引数はキャッシュする関数が依存する値の配列 • 配列の各要素をObject.is()で比較する • useEffect()のdepsと似ている (省略は不可) • 戻り値はキャッシュされた関数 • 前回のレンダリング時とdepsの全要素が等しければ キャッシュされた関数が返される • それ以外はuseCallback()に渡された関数が返される 223
  183. オブジェクト・関数の同一性 import React from "react"; import { Child } from

    "./Child"; const MemoizedChild = React.memo(Child); export Foo() { const point = React.useMemo(() => { x: 100, y: 200 }, []); const handleEvent = React.useCallback(() => {}, []); return ( <MemoizedChild point={point} handleEvent={handleEvent} /> ); } import React from "react"; import { Child } from "./Child"; const memoizedChild = React.memo(Child); export function Foo() { const point = React.useMemo(() => { x: 100, y: 200 }, []); const handleEvent = React.useCallback(() => {}, []); return ( <MemoizedChild point={point} handleEvent={handleEvent} /> ); } { x: 100, y: 200 } { x: 100, y: 200 } () => {} () => {} 等しい MemoizedChildは再レンダリングされない レンダリング1 レンダリング2 224
  184. useMemo()/useCallback()を使うとき • ホストコンポーネントに渡されるオブジェクト・関数 • ホストコンポーネントは実行されないので適用する必要はない • Reactコンポーネントに渡されるオブジェクト・関数 • Reactコンポーネントがメモ化されているなら適用する •

    useEffect(), useMemo(), useCallback()の依存配列に 渡されるオブジェクト・関数 • 常に適用する • 上記に該当するか自明でない場合は適用する • 3rd-Partyのコンポーネントに渡すオブジェクト/関数や 広く共有されるカスタムHooksに渡す/返すオブジェクト/関数など 226
  185. パフォーマンスとメモ化 まとめ • Reactコンポーネントの状態 (State) が更新されると そのコンポーネントと子孫コンポーネントのツリー全体が 再レンダリングされる • レンダーフェーズで実行されるコンポーネントの量が問題と

    なる場合がある • 再レンダリングされるコンポーネントツリーを小さくする • 異なるタイミングで更新される状態毎にコンポーネントを分割する • 更新される状態を持つコンポーネントをツリーの下の方に置く • 親コンポーネントが頻繁に再レンダリングされても 再レンダリングの必要が少ない子コンポーネントはメモ化する 227
  186. React研修 まとめ • 学んだこと • 現代のWebアプリの形態とReactが利用されてる状況 • Reactの特徴および関数コンポーネントの書き方、JSXの書き方 • コンポーネントの状態を更新する方法とその背後でのReactの動作

    • DOM以外のリソースを宣言的UIに沿った方法で扱う方法 • 仮想DOMではカバーしきれないレンダーフェーズの最適化 • 次のステップ • 実際にコードを書いて動作と頭の中のイメージを一致させよう • React公式ドキュメントを読んで正しい知識を得よう • ライブラリ、フレームワーク、ツールなどのエコシステムを知ろう • React Server Componentsなど次世代の機能を知ろう 228