りあクト! TypeScript で始めるつらくない React 開発 第 3.1 版【② React 基礎編】を読んで学んだことをメモしておきます。
りあクト!は 3 作あり、2 はフロントエンドの歴史や、React の基本的な知識について触れられています。
学んだこと
5 章 JSX
- JSX は JavaScript と XML の組み合わせ
- JSX は Babel や tsc でコンパイルされることを前提としたシンタックスシュガー
- JSX は第一引数にタグ、第二引数に属性のオブジェクト、第三引数に子要素の文字列を受け取る
React.createElement
に変換される(17.0 より前) - JSX はテンプレート言語ではなく、単なるオブジェクトを表現する式なので、変数に代入したり、関数の引数や戻り値にできる
- JSX は React 固有のものではなく、Babel のプリセットの設定を変更すれば他のフレームワークでも使える
- アーキテクチャやデザインパターンは関心の分離を行うためにあるもの
- MVC は関心の分離の単位が技術の役割
- 機能単位で分割された独立性の高いパーツ(コンポーネント)を組み合わせることでアプリを構築するのが React の開発思想
- コンポーネントは疎結合にするために、見た目とロジックを閉じ込める必要がある
- コンポーネントの処理は自分自身で行い、レンダリングも個別で行われる、さらにそれらはコンポーネント単位で並列に非同期で実行される
- SPA の開発でデザイナーが HTML や CSS のコーディングまで行うことは稀
- HTML テンプレートを使うフレームワークではフレームワーク独自の構文や各種バインディングなど覚えることが多いが、React は JS ファーストで、独自の決まり事が少なく JavaScript を活用できる
- React は制御構造が主体だからリファクタリングしやすい
- テンプレートはフレームワークによるコンパイルをはさむのでエラーが非常にわかりづらい
- JSX が純粋な式であることで静的解析や形推論に適しており、それによって IDE や Lint のサポートを受けやすく、TypeScript との相性が良い
- React はレンダラーを本体から分離する設計になっていて、レンダラーを入れ替えることでさまざまなプラットフォームのアプリやドキュメントに対応できる
- React が対応しているプラットフォームのコア部分は仮想 DOM を view に反映させるレンダラーのバリエーション
- レンダラーの共通の記述言語が JSX
- JSX で
React
が使われているので、React
自身のインポート文が必要 - JSX の
{}
では null、undefined、true、false は出力されない - JSX で子要素として文字列を記述する場合、空白行と、行の先頭と末尾の空白文字は削除される
- props で true を渡す時は true は省略して属性値だけで渡すことができる
- JSX は
aria
とdata
属性だけケバブケースで書く - div や p などの HTML も JSX では組み込みのコンポーネントに変換される
6 章 Lint とフォーマッタ
- JavaScript の Linter の歴史 JSLint → JSHint → ESLint・JSCS → ESLint・TSLint → TypeScript ESLint
- CRA のプロジェクトには最初から ESLint のパッケージがインストールされている
- CRA の ESLint は react-scripts との相性もあり、そのままのバージョンを使った方が良い
typescript-eslint-parser
とeslint-plugin-typescript
は非推奨- ESLint 本体を除くエコシステムのパッケージは Parser、Plugin、Shareable Config の 3 種類
- ESLint にとってプラグインはルールのこと
- ESLint の設定ファイルは JavaScript が無難
- すべてのファイルを ESLint でチェックすると重くなるので、必要ないファイルは対象外にする
eslint-disable
はそのコメント以降からファイルが終わるまでチェックを無効化するコメント- ESLint でも
--fix
を指定するとコードを整形できる - Prettier では一律のスタイルを決めて、開発者がカスタマイズできる幅を制限している
- Prettier を ESLint の拡張プラグインとして動作させる
eslint-plugin-prettier
は非推奨になった - ESLint の環境に Prettier を加えるには
prettier
本体と、eslint-config-prettier
が必要 eslint-config-prettier
系の共有設定は、他と競合するルール設定を上書きして調整するものだから最後に書く- CSS の Linter の歴史 CSSLint → CSScomb → stylelint
- stylelint は ESLint の CSS 版で現在の CSS の Linter の主流
- stylelint で必要なパッケージは
stylelint
、stylelint-config-standard
、stylelint-order
、stylelint-config-recess-order
- stylelint は
.stylelintrc.js
で設定する - ESLint の本体やプラグインはバージョン更新で変更が入る可能性があるので、不要な拡張ルールセットやプラグインは避けて、常に把握できる設定にしておく
- Husky を導入することで、Git の commit や push の前に lint のチェックができる
- lint-staged を導入することで、state 環境にあるファイルに対して lint のチェックができる
7 章 フロントエンドの歴史
- JavaScript はもともとホームページを無意味に飾りつけるための言語くらいにしか思われていなかった
- Ajax を使った Google マップの登場によって JavaScript が評価される
- prototype.js は JavaScript アプリ開発のためのさまざまな機能を提供するフレームワーク
- jQuery はブラウザ間の差異を吸収するライブラリ
- prototype.js や jQuery は DOM 操作ライブラリを主軸としながらアーキテクチャを持たないライブラリの詰め合わせで、既存の HTML をベースにその一部を動的に書き換える形式で開発する(第一世代)
- HTML5 の登場によって Flash の開発が中止される
- JavaScript エンジンである V8 が登場し、翌年に V8 エンジンを採用した JavaScript の実行環境である Node.js がリリースされる
- ES5 が発表
- アーキテクチャが存在しない第一世代の技術で高度なアプリを作ろうとすると無秩序に膨れ上がるので、サーバーサイドで一般的だった MVC をクライアントサイド向けにアレンジした Backbone.js、Knockout、AngularJS が登場(第二世代)
- Angular は SPA をターゲットにしたフレームワークだったが、性能的な問題と React の登場によって廃れていった
- React の登場 BoltJS(Facebook の内製フレームワーク) → FaxJS(React のプロトタイプ) → FBolt(BoltJS + FarJS) → React → オープンソース化
- クラスコンポーネントから関数コンポーネントに
Hooks の登場で関数コンポーネントが主流に
Rails でも最初は prototype.js をデフォルトの JavaScript ライブラリとしていたが、バージョン 3.1 から jQuery に乗り換えている
- AngularJS と Angular は全く別のフレームワーク
- React の 6 つのキーワード
- 宣言的
- コンポーネントベース
- UI にしか関知しない
- 仮想 DOM
- 単方向データフロー
- 一度習得すればあらゆるプラットフォームで開発できる
- 宣言的とは、どんなデータが表示されるべきかを記述しておくだけで React がそこにそのデータを表示し、適切なタイミングで適切な表示に更新してくれるということ
- 関数型によってコンポーネントやデータフローまで宣言的であろうとしている
- 宣言的であることでコードが読みやすく挙動が予測しやすくなり、テストやデバッグもやりやすくなる
- React では MVC のような従来のデザインパターンは存在せず、適切にコンポーネントを切り分け、データフローの規範に従っていれば model や controller は必要ない
- Vue.js では MVVM パターンなので view model とコンポーネントが 1 対 1 であることが求められるため、コンポーネントの切り分けが難しくなる
- React がフルスタックのフレームワークではなく、ライブラリであることで周辺技術の変化に対応しやすくなる
- 仮想 DOM は純粋なレンダリングの速さを求めるための技術ではなく、DOM を効率的に更新して高い DX を実現しつつアプリのレスポンス性を高める技術
- サーバーサイドでは 1 回のリクエストに対して 1 回の view レンダリングで済むけど、ブラウザではイベントに対してリアルタイムで view を書き換える必要がある
- 仮想 DOM は、変更前の DOM ツリーがメモリにキャッシュされ、ツリーのどこかの要素が変更されると、レンダリングが再実行されて、新しい仮想 DOM が作られ、変更前と新しいものを比較して差分だけをリアル DOM に反映させる
- 処理が走っても DOM に差分がなければ余分な再レンダリングが発生しない
- 仮想 DOM は最初の構築にはオーバヘッドがある
- 双方向データバインディングは制御ロジック側での値の変更が view へ埋め込んだデータに伝搬されると同時に、view 側での値の変更が制御ロジックのデータに反映されるデータフロー
- 双方向データバインディングでは、model のデータの変更が別の model のデータの更新につながることで、単一のユーザーインタラクションの結果として何が変わるのかを予測するのが困難になる
- 双方向データバインディングは直感的だが、大規模のアプリでは複雑になる
- React における単方向データフローとは、親から子への一方向でデータが props として流れ落ちることで、その逆の流れはない
- 単方向データフローは関数型と相性が良い
- React・Angular・Vue.js(第三世代)
- Angular は保守的なフルスタックのフレームワーク
- Vue.js は AngularJS の機能から不要なものを削ぎ落とし、データバインディングなどの最低限の機能に絞った軽量なフレームワーク
8 章 コンポーネント
- コンポーネントが通常の関数と違うのは、状態を保つことができること
- 関数コンポーネントは仮想 DOM の差分検出処理エンジンによって React Elements ごとに状態を保持する空間が用意される
- コンポーネントのレンダリング差分が発生するかどうかは props と state だけを見ればいい
- state は極力コンポーネントに持たせるべきではなく、state を持つコンポーネントの数は最小限に抑えるべきかつ、一つのコンポーネントが持つ state の数も最小限にすべき
- state は副作用の原因になる
FC
は React の関数コンポーネントの型インターフェースFC
に型引数を渡すことで、そのコンポーネントの props の型を指定できるFC
の型引数はデフォルトで{}
となっている- ライフサイクルが必要な理由は、コンポーネントのライフサイクルにおける任意のフェーズに処理を登録しておいて、そのタイミングで実行したことがよくあるから
- ライフサイクルのフェーズ
- Mounting
- Updating
- Unmouting
- Error Handling
- ロジックから独立して見た目だけを責務としたコンポーネントを分離しておけば、それらをスタイルガイドに登録してデザインの運用に活用できる
- presentational component を作ってからロジックを追加して container component を作ることは React の流儀に沿ってコンポーネントを作ることにつながる
9 章 Hooks、関数コンポーネント
- コンポーネントから state を伴ったロジックを切り出して再利用するためのものが Hooks
- React 当初はクラスコンポーネントしかなく、通常のクラス定義ではなく、
React.createClass
という静的メソッドを使って生成していた mixins
はコンポーネントと依存関係を持ってしまい、また名前の衝突が起きやすいmixins
の代わりに推奨されたのが HOC という、コンポーネントを引数に取り、戻り値としてコンポーネントを返す関数である高階コンポーネント- Render Props とは React Elements を返す関数を props として受け取って、それ自身のレンダリングに使う特殊なコンポーネントを使った手法
- Render Props も HOC も props に外側のコンポーネントから状態やロジックを注入したい
- HOC や Render Props は既存の技術を利用した設計パターンだったが、Hooks は公式が新たに React の機能として追加したもの
- Hooks は状態やロジックの分離を小手先のテクニックではなく、新しい仕組みを仮想 DOM の外に用意したもの
- HOC や Render Props の問題はロジックの追加によってコンポーネントツリーの階層が深くなり、汚染されてしまうこと
- Hooks だと、状態をもったロジックをコンポーネントから完全に切り離し、単独でテストしたり、別のコンポーネントで再利用できる
- ライフサイクルメソッドは処理の流れを追いづらく、機能単位でのロジックの切り出しが難しいため、Hooks では時間ではなく機能によって処理をまとめる
- 外部 API からデータを取得して state に格納する場合は型推論ができないので、明示的に型引数を渡す必要がある
- state の初期値に明示的に null を入れたい場合の型定義は、
useState<User | null>(null)
のように書く - state を相対的に変更する処理を書く時に、前の値を直接参照・変更するのは避けて
setCount((c) => c + 1)
のように書く - Hooks の state はコンポーネントのレンダリングごとで一定で、クラスコンポーネントは
this.state
に常に最新の値が入る - 副作用とは、コンポーネントの状態を変化させ、それ以降の出力を変えてしまう処理
- useEffect の第一引数として渡す関数がその戻り値として任意の関数を返すようにしておく、コンポーネントがアンマウントされる時にその関数を実行する
- useEffect の第二引数の依存配列を省略するとレンダリングごとに第一引数の関数が実行される
- useEffect の中で毎回のレンダリングに state を書き換える処理があると無限ループになる
componentDidMount
はコンポーネントがマウントされて仮想 DOM からリアル DOM へ反映される前に、ブラウザの表示をブロックして実行されるので、時間がかかる処理があると、その処理が終わるまでコンポーネントの部分が何も表示されないuseEffect
の初回実行では、最初のレンダリングが行われてその内容がブラウザに反映された直後に、あらためて副作用が反映された内容で再レンダリングされる- 関数コンポーネントのレンダリングとは、関数が最初から最後まで実行されること
- コンポーネントに定義されている変数や関数も毎回の実行のたびに定義し直され、実行が終われば破棄される
- 関数コンポーネントで useState が使われている場合、レンダリング後の state がコンポーネントの外で保存され、次のレンダリングが始まるタイミングであらためてコンポーネントに渡される
- クラスコンポーネントはマウントからインスタンスが生成されてアンマウントまで生き続けるが、関数コンポーネントはレンダリングのたびに実行されては破棄される
- props と state はクラスコンポーネントではインスタンスのメンバー変数の形でレンダリングとは関係なく内部のミュータブルな値として保持されるのに対して、関数コンポーネントではレンダリングのたびに外からイミュータブルな値として与えられる
- ライフサイクルメソッドではこのタイミングでこの処理を実行するという命令的で、
useEffect
はこの副作用処理はこの条件との t 基に実行されるべきという宣言的 - 関数はプリミティブ型と違って、内容が同じであっても定義ごとに指しているメモリのアドレスが異なるので、比較すると別の関数であると判断される
- presentational component で何を props にすべきかは、JSX の中で使われているものだけを抽出すればいい
- 保守性が高いコードを書くには、presentational と container を分け、ロジックを切り出し、container の中で presentational とロジックを繋げる