はじめに
を参考にしながら開発しながら思ったメモ
(※当面随時更新)
メモ
実装のベースとその理由
第13章のSuspenseにページルーティングはv6を使う。
まだ本リリースはしていない機能だけれど実装の印象だけで判断。
外部公開するものでもないので ライブラリが安定版であるかどうかじゃなくて直感的にわかりやすい書き方ができる方を優先しようと割り切り。
技術課題があったら、あったでその時考える。少なくとも書籍の範疇であれば「動くらしい」ということは保証されている割り切り。
本を読みながらとったメモ
- useStateで状態と状態変更をする
- useEffectで状態遷移(第2引数が監視対象の変数)を管理してステートパターン的に処理をする
- useMemo を使って計算結果をコンポーネントシステムの外に保存しておく。依存配列が変わった時だけ再計算する。
- useCallbackは関数のメモ化。関数を共有したいとき&依存配列が変わった時だけ再処理したいときに使う。
- useRefは初期ロード時にリアルDOMへの参照を取得して処理する(初期フォーカスとか)&再レンダリングを伴わずに何らかのデータを関数コンポーネントで保存しておくときにも使える
- useRef は useState と違って値の変更がコンポーネントの再レンダリングを発生させない
- Custom Hook
関数の名前の頭に『use』をつける - SPA におけるルーティングとは『DOM の動的な書き換えによってページ遷移を擬似的に実現するとともに、ブラウザのセッション履歴をそれに同期させること』
- ほとんどの場面では Route は Switch といっしょに使う
- <a> タグを使って書くと、そのリンクを踏んだ時点で React Router の管轄外となり管理していた履歴がすべて消える
(Web サーバにリクエストが飛んで、SPA のコード全体がリロードされるため) - リンク先を設定する属性の to にはパスの文字列またはlocation オブジェクトを渡すことができる。
location オブジェクトならパスの他にクエリパラメータやハッシュも設定できるし、ユーザーに見せたくない情報を埋め込んでリンク先に受け渡すこと ができる - boolean 型の属性である replace を指定すれば、クリックした時点でページの履歴が消える
- useRouteMatch なら match オブジェクトをまるごと取得できる。マッチした URL パラメータにしか用がないときは useParams を使ったほうが便利
- 『Atomic Design』という UI デザイン手法を適用
Atoms、Molecules、Organisms、Templates、PagesのUI パーツを分ける - React Helmet
ライブラリで、どこからでも HTML ドキュメントヘッダを動的に上書き
SPA では意図して書き換えないと常に public/index.htmlに書かれた <title> の中身がどこのページでもそのページタイトルになるので注意 - 任意のコンポーネントでいつでもルーティングできる(できてしまう)
- useEffect を使ってコンポーネントの初回レンダリング時に強制的にトップにスクロールさせる(という実装例あり)
- Modelじゃなくてコンポーネントを主軸にしてフロントエンドは実装するみたいなので設計や実装の考え方を変える必要あり
- useReducerで状態変更の手段を複数持たせられる
-- (状態管理の手段が単一の場合はuseState、複数の場合はuseReducerをつかう?) - useContextで状態を扱う
--(Recoilもあるけどそちらは使わないことにする。Providerで親から渡してuseCallbankで振る舞いを実装) - 状態変更からリダイレクトでページ遷移するときは「Navigate」でパス指定する
ページ遷移
App.tsx にまとめて書く
Routerを作って、Appから呼び出すという実装にはしない
RoutingはContainerの仕事(だからAppの仕事でもある)
LinkとNavigateはComponentで明示するとよい?(ページ遷移の副作用はViewで分かったほうが分かりやすい?)
パッケージ構成
- ComponentとContainerの違いを意識する
- https://qiita.com/ShiratoriTenta/items/7b4d2ea1a4d1d2336032
- Reactに関係するものだけをComponentとContainerに格納
--(それ以外はdomainという感じでフォルダを分ける) - contextはファクトリーと型をペアにして作成する(Containerフォルダに入れる)
- (複数ページ、複数機能でのパッケージ構成は1つ目を作ってから改めて考える)
ファイル構成
- Container側の関数名に「Enhanced」などを付与して名前衝突を回避する
(Componentだけで画面構築ができるケースもある=Container側がオプショナルになると考えから)
実装の順番(ToDoメモ的なもの)
初回の作成順序
一回作って全体の工程を把握しつつ実際に動くものを作る流れの整理
- ベースのページ枠を作る(Component)
- ページ遷移を試す(単純なリンク)
- ページ内の動作や動的書き換え部分を作る(状態遷移で宣言的に)
-- (ここまではデータ分離はしていない) - データ(固定)の埋め込み動的なモック状態へ
-- (この時点ではコンポーネントを分割しないで割とベタに1つのファイルでゴリゴリ)
--(ここまではコンポーネントの切り替えのContext以外は使用していない) - ページ内で使用するモデル(type)を作る
- アプリの主要な機能を実装
--(機能実現のための状態管理を含めて実装) - 共通機能であるタイマーコンポーネントを作成して埋め込む
-- (ここまででデザインはともかくとして主要な機能は出来ている) - ファイル分割をする
- CSS in JSを使ってデザイン(Component)を組み入れる
- StoryBookを使ってコンポーネントのデザインを確認できるようにする
- 状態によってCSSを変えたい部分の追加実装(状態遷移)
- テストを実装
- (E2Eは迷い中。まずはそれ以外かな。)
2つ目のアプリに向けて
- monorepo構成に変更
- アプリ共通部分と個別の部分を分ける
- buildをして意図した資産出力がされるか確認する
- script周りを整理(test)
- パッケージの役割と実装パターンを整理
-- (動くだけモノから、次の開発プロセス的やアーキテクト的な整理をちゃんとする)
2つ目アプリ
(ざっくり。1つ目を作った後で見直す)
- ComponentとStoryBookで表記コンポーネントだけを実装
- モデル(type)を作成
- 検証用のデータを作成
- Containerを作成
- Reducer(とContext)で作成(状態管理を実装)
- ContainerとContextを連携
- StoryBookを修正(Container分割でPageのStoryBookが壊れているため)
- テストを実装(最低限はStoryShot)
理解のイメージ
AppはルートとなるContainer。次にPageのComponent。
部品となるPanelのContainerへ問いかけて ViewとなるComponentを受け取って表示。
これを繰り返したミルフィーユでReactは画面を構築する。
僕にとってなじみの画面構築はテンプレートということが ようやくわかった気がする。テンプレートはViewのパーツを組み合わせて、そこからContainerに相当するアクションを呼び出す。
(思考が1つ前に進んだ気がする。)
実装の流れ
containerとComponentの往復をしながら作り込んでいく。
はじめは往復が面倒だったけど、フォルダであれファイルであれ デザインと実装の分離はStoryBookの利用も考えると分けておきたい。
いずれにしても何かしらの分離は必須だから多かれ少なかれ往復は発生するので心理的には許容範囲。
(当初は「理解のイメージ」がピタッときていなかったけれど、それがイメージできたら急に読みやすくなった。)
イベント処理
useKeyPress React Hook - useHooks
お作法
命名
- React では、イベントを表す props には on[Event] という名前、イベントを処理するメソッドには handle[Event] という名前を付けるのが慣習となっています。
- コンポーネントが自身の key について確認する方法はありません。
- 配列のインデックスを使う場合と同様な問題が生じるためほとんどの場合は推奨されません。
key はグローバルに一意である必要はありません。コンポーネントとその兄弟の間で一意であれば十分です。
Reducer(もしくはAction/dispatch)
ステートの扱い
Reduxを使わなくとも、useReducerを使うにあたって実装パターンとして踏襲するべきように思う。
* 書籍(3部 P62近辺)
* (prevState,action) => nextState
* actionが同じなら「差分も同じ」ことが保証される
Reducer ファイルの基本構造
関数毎にファイルを分けてもいいけど、1つにまとめて良いかな
const xxxReducer = (state: StateType, action: ActionType): StateType
のところでJSDocでActionでやるべきことを書きながらステートとアクションとその詳細で何をするのか先に書いておいてから具体的な実装をする感じ。
実装をしながら必要なステートがあったら適宜追加していく
// type ValueOf<T> = T[keyof T]; export const actionKeys = [ 'initialized', ] as const; export type PayloadType = { value: string; }; export type ActionType = { type: typeof actionKeys[number]; payload: PayloadType; }; export type StateType = { stateValue: typeof actionKeys[number]; value: string; }; export const initializedState = (): StateType => { return { stateValue: 'initialized', value: ``, }; }; const xxxReducer = (state: StateType, action: ActionType): StateType => { switch (action.type) { /** * 状態初期化 */ case "initialized": { return { ...state, }; } /** * 上記以外のアクションは状態変化なし */ default: { return state; } } }; export default xxxReducer;
関連資産の雛形もついでに
import { createContext } from 'react'; import { ActionType, StateType } from './XXXReducer'; export type XXXContextType =StateType & { dispatch: React.Dispatch<ActionType>; }; export const XXXContext = createContext<XXXContextType>( {} as XXXContextType, );
import React, { FC, useReducer } from 'react'; import {XXXContext} from "./XXXContext"; import xxxReducer, { initializedState } from './XXXReducer'; const XXXProvider: FC = ({ children }) => { const [{ value }, dispatch] = useReducer(XXXReducer, initializedState(),); return ( <XXXContext.Provider value={{ value, dispatch, }} > {children} </XXXContext.Provider> ); }; export default XXXProvider;
StoryBook ファイルの基本構造
ページ全体を確認するStoryBook
PageStoryを使うことで、Providerで対象ページコンポーネントを挟み込む
Contextから情報を取得しないで良いものには不要
import React, { FC, ComponentProps } from 'react'; import { Story } from '@storybook/react/types-6-0'; import XXXComponent from './XXXComponent'; type PageProps = { value: string; }; const PageStory: FC<PageProps> = (args: PageProps) => { return ( <> <XXXComponent value={args.value} /> </> ); }; export default { title: 'page-name/Page', component: PageStory, }; const Template: Story<ComponentProps<typeof PageStory>> = (args: PageProps) => ( <PageStory value={args.value} /> ); export const FirstStory = Template.bind({}); FirstStory.args = { value: '', };
useEffect完全ガイドを読みつつのメモ(抜粋のみ含む)
- Effectは props や state の変化を DOM にシンクロさせるという思考で
- props や state を必要としない関数はコンポーネント外にして、エフェクトでしか使われない関数はエフェクト内に入れる
- エフェクトがレンダースコープ内の関数を使うことがあるのであれば(props からの関数も含む)、 useCallback で関数が定義されている場所をラップしてそのプロセスをリピート
さいごに
開発日誌的なTweetをぶら下げます
TypeScriptとReactってことで やってみたい構成なので購入
— Yamashita (@_vermeer_) October 6, 2020
とりあえず、本を買ったので一歩前進 https://t.co/mCi5HqfvDJ
2021年のスタートで、間違えて、JSFツイートにぶら下げてしまったので そちらのリンク
JSFもコンポーネント指向なので、ライブラリの進化(?)としてはClassではなくFunction的な実装記述ができる拡張ができたら面白いかもしれないと、Reactを学びながら思ったりする(具体的にどうっていうのはノーアイディア)
— Yamashita (@_vermeer_) January 4, 2021
まぁそれをするんだったら素直にReact使った方が良い気がしますが(苦笑)