Redux Style Guide
日本語訳ではなくasRagi.iconによる解釈
◆必須
状態をMutableにしない
redux-immutable-state-invariantで状態の変化を監視できる.
Immerを用いると意図しない状態の変化を回避できる.
Reducerは副作用を持たない
純粋関数として実装する.
stateとactionのみに依存し,それらのみに基づいて返り値を決定する.
非同期処理やDate.nowなど値が不定な処理を実行しない.
Reducerの外部に影響する処理を実行しない.
console.logなども副作用だが,グレーゾーンである.
シリアライズ可能でない値をstateに持たせない
Promiseや関数,Mapsやクラスインスタンスが該当.
DevToolsでデバッグしやすいように.
Reducerに渡される前のActionにシリアライズ不可な値を持たせても,Reducerに到達しないなら良い.
Redux Thunkやredux-promiseなどを用いる場合に.
Storeはアプリケーションに1つ
store.jsのように独立したファイルで管理する.
storeにロジックを持たせず,Providerを用いてアプリケーションに渡すか,thunksなどを用いて間接的に参照する.
◆強く推奨
redux-toolkitを使ってReduxのロジックを書く
Immerを使ってImmutableに書く
redux-toolkitの一部として利用することも
feature folderを用いて機能毎にReducerやActionをまとめる
Reducerをまとめるフォルダ,Actionをまとめるフォルダ,ではなく,機能ごとにまとめる.
Ducksパターンを採用する手もある.
単一機能のReduxのロジックを同一のファイルに配置する.
例
/src
index.tsx: Entry point file that renders the React component tree
/app
store.ts: store setup
rootReducer.ts: root reducer (optional)
App.tsx: root React component
/common: hooks, generic components, utils, etc
/features: contains all "feature folders"
/todos: a single feature folder
todosSlice.ts: Redux reducer logic and associated actions
Todos.tsx: a React component
できるだけロジックをReducerに書く
ロジックを書いてからdispatchするのではなく,シュッとdispatchするとReducerに書かれたロジックがいい感じにやってくれるようにする.
アプリケーションのロジックとReduxのロジックが分離されtestabilityが確保される.
Reducerは純粋関数なのでロジックがReducerに書かれていると嬉しい.
外部にロジックがあると状態の更新をMutableにやってしまいかねない.
Immerの恩恵も受けられる.
Reducer単位での操作のやり直しやホットリロードによるデバッグが可能である.
状態の更新に関わるロジックがどこに書かれているか一目瞭然となる.
Reducerが状態の形状を決定する
Reduxのroot stateは単一のroot reducerによって決定される.
ReducerはKey-Valueの「スライス」によって分割され,それぞれの分割された「スライスreducer」はそれぞれの「スライスstate」の初期値及び更新を担当する.
return action.payloadやreturn {...state, ...action.payload}のようにスプレッド構文を利用すると,更新後の状態の形状が渡されてきたactionやstateに依存してしまう.
code:example.js
const initialState = {
firstName: null,
lastName: null,
age: null,
};
export default usersReducer = (state = initialState, action) {
switch(action.type) {
case "users/userLoggedIn": {
return action.payload;
}
default: return state;
}
}
// usersReducerに以下のdispatchが飛んでくると破滅する
dispatch({
type: 'users/userLoggedIn',
payload: {
id: 42,
text: 'Buy milk'
}
});
静的型付けでうまくやっていこう
Storeのデータに基づいてState Slicesに名前をつける
combineReducersを用いることでReducerをまとめることができる.
combineReducersに渡されるobjectのkeyが最終的なstateのkeyの名前を決定する.
{userReducer: {}, postsReducer: {}}ではなく,{users: {}, posts: {}}という形でstateが保存されるべきである.
コンポーネント単位ではなくデータ型に基づいてstateを構造する
Storeのデータとコンポーネントは必ずしも1対1でない.
どのコンポーネントもuserを参照するなど
Reducerをステートマシンとして扱う
現在の状態に基づいてactionをどう扱うかをReducerで決定する.
現在の状態に基づかない場合,「データを編集中」でないにも関わらず「データを更新」のactionが通ったりする.
有限オートマトンを利用していく.
例
fetchUserReducerは以下の状態を取ると定める.
"idle","loading","success","failure"
有限状態をstateのフィールドに持たせる.
make impossible states impossible
code:state.js
const initialUserState = {
status: 'idle',
user: null,
error: null
};
TypeScriptならunion型が適切
各状態に対するReducerを実装して状態毎に使う
code:state.js
import {
FETCH_USER,
// ...
} from './actions'
const IDLE_STATUS = 'idle';
const LOADING_STATUS = 'loading';
const SUCCESS_STATUS = 'success';
const FAILURE_STATUS = 'failure';
const fetchIdleUserReducer = (state, action) => {
// state.status is "idle"
switch (action.type) {
case FETCH_USER:
return {
...state,
status: LOADING_STATUS
}
}
default:
return state;
}
}
// ... other reducers
const fetchUserReducer = (state, action) => {
switch (state.status) {
case IDLE_STATUS:
return fetchIdleUserReducer(state, action);
case LOADING_STATUS:
return fetchLoadingUserReducer(state, action);
case SUCCESS_STATUS:
return fetchSuccessUserReducer(state, action);
case FAILURE_STATUS:
return fetchFailureUserReducer(state, action);
default:
// this should never be reached
return state;
}
}
ネストされたStateでも正規化する
Stateを最小限にして計算して出せる値は計算して出す
計算はSelector関数で行う.
Selectorによる計算結果ははメモ化すると良い.
reselectやproxy-memoizeなどのライブラリを利用する.
Actionはsetterではなくイベントとしてモデルする
イベントを「setter」ではなく「occured」として扱う.
code:event.js
// Action described by Event
const eventObject = { type: "food/orderAdded", payload: { pizza: 1, coke: 1 } };
// Action described like setter
const pizza = {
type: "orders/setPizzasOrdered",
// Clients should know current state.
payload: { amount: getState().orders.pizza + 1 }
};
Actionには意味のある命名をする
action.typeはReducerの識別子であるだけでなく,Redux DevToolsに表示される名前でもある.
多くのReducerが同じActionに対応できるようにする
Actionをイベントとしてモデルし,多くのReducerがそのActionに応答できるようにする.
実際にはほとんどのActionはひとつのReducerに処理される.
たくさんのActionを順次Dispatchしてトランザクションのようにすることは避ける
余分なレンダリングの発生
中間のDispatchが無効になるリスク
イベントとして全てのstateが同時に更新されるようにする.
それぞれのStateをどこに生存させるか評価する
Three Principles of Reduxでは一つの木でStateを管理するように言われているが,それはあらゆる値をReduxで管理しなくてはならないということではない.
アプリケーション全体にわたるグローバルな値は一括で管理する.
ローカル変数はコンポーネントで管理する.
React-Redux Hooks APIを利用する
ReactコンポーネントからStoreを利用する場合は基本的にReact-Redux Hooks APIを利用する.
react-redux connectに比べて,TypeScriptとの親和性も高く,コード量も小さく済む場合が多い.
パフォーマンスやデータフローにやや弱点があるが,それでも推奨される.
react-redux connectは高階コンポーネントである.
コンポーネントをwrapしてpropsとしてデータを渡す.
useSelectorやuseDispatchは単一のSelectorのみを受け付けるため,TypeScriptによる型付けが簡易になる.
React-Redux Hooks API docsを読もう.
Storeからデータを読み取るためにたくさんのコンポーネントを接続する
より小さい粒度でコンポーネントをStoreに接続する.
UIのパフォーマンスの改善につながる.
レンダリングが小さい規模で発生するようになるため.
<UserList>で全ユーザのデータを取得して一覧表示するのではなく,<UserListItem userId={userId}>がそれぞれStoreに接続しユーザのデータを取得する.
connectでmapDispatchのオブジェクト短縮形を利用する
connectへのmapDispatch引数は「dispatchを引数で受ける関数」あるいは「Action生成を含むオブジェクト」として渡すことができる.
オブジェクト短縮形として渡すことでコードがシンプルになる.
関数としてmapDispatchを書くことはほぼない
オブジェクトとして渡すドキュメント
https://react-redux.js.org/using-react-redux/connect-mapdispatch#defining-mapdispatchtoprops-as-an-object
FunctionalComponentの中で何度もuseSelectorを呼び出す
少数の大きいuseSelectorではなく多数の小さいuseSelectorを用いて複数のオブジェクトを返す.
小さい単位でレンダリングできるようになる.
とはいえ,細かすぎるのも良くないのでうまくバランスする.
静的型付けをする
TypeScriptやFlowを利用する.
React-ReduxもReduxもTypeScriptやFlowに対応している.
redux-toolkitはTypeScriptに対応している.
Redux DevTools Extensionを用いてデバッグする
以下のデバッグが可能
dispatchのログ
actionの内容
ActionがDispatchされた後のState
Action前後のStateの差分
ActionのDispatchに関するスタックトレース
さらに,Actionの履歴を前後させてStateの変遷やUIの変化を確認することができる.
プレーンなJavaScriptオブジェクトをStateに入れる
Immutable.jsなどのライブラリを用いず,JavaScriptのオブジェクトをStateに用いる.
データ型の変換が複雑になってしまうリスクが低減する.
バンドルサイズを小さくする.
redux-toolkitの一部としてのImmerの利用は推奨される.
◆推奨
Action.typeにはdomain/eventNameの書式で命名する
redux-toolkitのcreateSliceではtodos/addTodoのような形式で指定する.
Fluxの慣習に沿ってActionを書く
ActionはAction.typeフィールドを持つことしか原典のFluxでは規定されていなかった.
Flux Standard Actionsにて通例が作られた.
https://github.com/redux-utilities/flux-standard-action
要約すると以下の通り
action.payloadにデータを入れる
action.metaに追加の情報を入れる
action.errorに処理が失敗した場合の情報を表現する
Reduxのエコシステムは上記の通例に則っている.
redux-toolkitもそう.
Flux Standard Actionsでは成功時に対して,失敗時はerror: trueとフラグを立て,同じAction.typeを書くことが推奨されているが,実際には別々のAction.typeを書くことも多く,それでも良い.
Action Creatorを利用する
Flux Architectureでは用いられていた手法だが,Reduxでは必須とされていない.
コンポーネントもロジックもActionを書ける.
Action Creatorを利用するとIDの発行などの処理に一貫性を持たせることができる.
redux-toolkitのcreateSliceを利用してAction CreatorやAction.typeを自動的に生成することが推奨される.
非同期処理にはRedux Thunkを利用する
デフォルトで用いることが推奨されている.
大体のユースケースには対応する.
バックグラウンドで非同期処理や,debounce,キャンセルなど本当に複雑な非同期処理を書く場合,Redux-SagaやRedux-Observableなどより強力なライブラリの利用を検討する.
Reduxは拡張性が高く設計されており,APIを介して様々な形でStoreと接続できるようになっている.
RxJSなどを学ばなくても良い.
複雑なロジックはコンポーネントの外に出す
Presentational&ContainerComponents
特に非同期処理はRedux Thunkを利用する.
特にStoreからデータを引き出す場合に.
React Hooksで十分なこともある.
Selectorを利用してStoreからStateを読む
Reselectのようなライブラリを用いると,入力が変化した場合にのみ再計算を行うようにSelectorをメモ化して利用することができる.
あらゆるStateのフィールドに対してSelectorを使わないといけないということではない.
フィールドが参照される頻度や更新される頻度に応じて粒度を決める.
Selectorにはselectのprefixをつける
selectVisibleTodosみたいに命名する.
フォームの状態をReduxに入れない
useStateとかでやるべき.
参考
https://redux.js.org/style-guide/style-guide