この値はどこから来て、どこで更新されるのか
前回は、Reduxの基本として、store、state、action、dispatch、reducer、selector の関係を整理しました。
Reduxの基本的な流れは、次のようなものでした。
dispatch(action)
↓
reducer(currentState, action)
↓
newState
↓
storeに保存
↓
画面が再描画される
ただ、実際の既存コードを読むときは、もう少し難しく感じます。
Reduxまわりのコードは、プロジェクトによって次のように複数のファイルに分かれていることが多いです。
- component
- container
- selector
- action
- action creator
- reducer
- store
そのため、いきなり全体を理解しようとすると、
- この値はどこから来ているのか
- どこで更新されているのか
- どのactionが関係しているのか
- reducerでは何が変わっているのか
が分からなくなりがちです。
そこで今回は、Reduxを使った既存コードを読むときに、どの順番で追えばよいのかを整理します。
まずは1つの値に絞って追う
Reduxの既存コードを読むときは、最初から全体を理解しようとしないほうがよいです。
まずは、画面上の1つの値に絞ります。
たとえば、検索キーワードを入力するフォームがあるとします。
<input
value={keyword}
onChange={(event) => {
dispatch(changeKeyword(event.target.value));
}}
/>
このとき追いたいことは、次の2つです。
keyword はどこから来ているのか
keyword はどこで更新されるのか
この2つが分かると、Reduxの流れがかなり見えやすくなります。
Reduxの既存コードを読むときは、「画面全体」ではなく「1つの値」から追うのがおすすめです。
Reduxコードを読む順番
Reduxのコードを読むときは、次の順番で追うと理解しやすいです。
1. 画面で使われている値を見る
2. selectorを見る
3. dispatchしている箇所を見る
4. action creatorを見る
5. reducerを見る
6. state更新の流れを一本につなげる
この順番で見ると、次の流れを追うことができます。
表示されている値
↓
Redux storeのどこから来ているか
↓
どの操作で更新されるか
↓
どんなactionがdispatchされるか
↓
reducerでどのstateが変わるか
1. 画面で使われている値を見る
まずはReactコンポーネントを見ます。
const keyword = useSelector(selectKeyword);
const dispatch = useDispatch();
return (
<input
value={keyword}
onChange={(event) => {
dispatch(changeKeyword(event.target.value));
}}
/>
);
ここで見るポイントは2つです。
value={keyword}
dispatch(changeKeyword(...))
value={keyword} を見ると、この入力欄は keyword という値を表示していることが分かります。
そして keyword は useSelector(selectKeyword) から取得されています。
const keyword = useSelector(selectKeyword);
つまり、この keyword はコンポーネント内の useState ではなく、Redux storeから取得している値です。
ここまでで分かることは、次のとおりです。
画面の入力値 keyword は Redux store から来ている
2. selectorを見る
次に、selectKeyword の中身を見ます。
const selectKeyword = (state: RootState) =>
state.searchForm.keyword;
selectorは、Redux storeのstateから必要な値を取り出す関数です。
このselectorを見ると、keyword は次の場所から取得されていることが分かります。
state.searchForm.keyword
つまり、storeの中身はざっくり次のような構造だと考えられます。
type RootState = {
searchForm: {
keyword: string;
};
};
この時点で、画面に表示されている値の出どころが分かりました。
inputのvalue
↓
keyword
↓
selectKeyword
↓
state.searchForm.keyword
既存コードで値の出どころが分からないときは、まず useSelector や mapStateToProps を探すとよいです。
3. dispatchしている箇所を見る
次に、値がどこで更新されるかを見ます。
先ほどのコードでは、onChange の中で dispatch しています。
<input
value={keyword}
onChange={(event) => {
dispatch(changeKeyword(event.target.value));
}}
/>
このコードから、入力欄が変更されると changeKeyword がdispatchされることが分かります。
ユーザーが入力する
↓
onChangeが発火する
↓
dispatch(changeKeyword(event.target.value))
Reduxでは、画面からstateを直接変更しません。
代わりに、actionをdispatchして、reducerにstate更新を任せます。
そのため、既存コードを読むときは dispatch を探すと、次の観点を追いやすくなります。
この操作で何が起きるのか
Reduxでは、dispatch がstate更新の入口になります。
4. action creatorを見る
次に、changeKeyword の中身を見ます。
const changeKeyword = (keyword: string) => ({
type: 'searchForm/changeKeyword',
payload: keyword,
});
これはaction creatorです。
action creatorは、actionを作る関数です。
上記のコードでは、changeKeyword('React') を呼ぶと、次のようなactionが作られます。
{
type: 'searchForm/changeKeyword',
payload: 'React'
}
ここで見るポイントは2つです。
type
payload
type は、何が起きたかを表します。
searchForm/changeKeyword
payload は、そのactionに必要なデータです。
変更後のkeyword
つまり、このactionは、次の出来事を表しています。
検索キーワードが変更された
この時点で、画面操作からactionまでの流れが分かりました。
ユーザーが入力する
↓
onChange
↓
dispatch(changeKeyword(value))
↓
{
type: 'searchForm/changeKeyword',
payload: value
}
5. reducerを見る
次に、reducerを見ます。
reducerは、現在のstateとactionを受け取り、新しいstateを返す関数です。
const initialState = {
keyword: '',
};
function searchFormReducer(state = initialState, action) {
switch (action.type) {
case 'searchForm/changeKeyword':
return {
...state,
keyword: action.payload,
};
default:
return state;
}
}
ここで、先ほどのaction typeを探します。
case 'searchForm/changeKeyword':
このcaseでは、keyword に action.payload をセットしています。
return {
...state,
keyword: action.payload,
};
つまり、searchForm/changeKeyword というactionがdispatchされると、state.searchForm.keyword が更新されます。
ここまで追うと、次のことが分かります。
どの操作で
どんなactionがdispatchされ
どのreducerで
どのstateが更新されるか
6. state更新の流れを一本につなげる
ここまで見た内容を、1つの流れにまとめると次のようになります。
ユーザーがinputに入力する
↓
onChangeが発火する
↓
dispatch(changeKeyword(event.target.value))
↓
action creatorがactionを作る
↓
{
type: 'searchForm/changeKeyword',
payload: 入力値
}
↓
reducerがactionを受け取る
↓
state.searchForm.keyword が更新される
↓
selectorが新しいkeywordを取得する
↓
Reactコンポーネントが再描画される
↓
inputのvalueに新しいkeywordが表示される
Reduxのコードを読むときは、このように流れを一本につなげると理解しやすくなります。
useStateと比べると何が違うのか
同じ検索キーワードの入力でも、useState だけなら次のように書けます。
const [keyword, setKeyword] = useState('');
return (
<input
value={keyword}
onChange={(event) => {
setKeyword(event.target.value);
}}
/>
);
この場合、値の流れはかなりシンプルです。
入力する
↓
setKeyword
↓
keywordが更新される
↓
再描画される
一方、Reduxでは少し手順が増えます。
入力する
↓
dispatch
↓
action
↓
reducer
↓
store更新
↓
selector
↓
再描画
一見するとReduxは遠回りです。
ただし、Reduxでは状態更新の流れが明確になります。
どんなactionが起きたのか
どのreducerが処理したのか
どのstateが変わったのか
を追いやすくなります。
複数の画面やコンポーネントで共有する状態の場合、この「追いやすさ」が重要になります。
Redux Toolkitの場合
現在のReduxでは、Redux Toolkitを使う書き方が公式に推奨されています。
Redux Toolkitでは、createSlice を使ってreducerとactionをまとめて定義できます。
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
type SearchFormState = {
keyword: string;
};
const initialState: SearchFormState = {
keyword: '',
};
const searchFormSlice = createSlice({
name: 'searchForm',
initialState,
reducers: {
changeKeyword(state, action: PayloadAction<string>) {
state.keyword = action.payload;
},
},
});
export const { changeKeyword } = searchFormSlice.actions;
export default searchFormSlice.reducer;
この場合、changeKeyword というaction creatorは createSlice によって自動生成されます。
コンポーネント側では、次のようにdispatchできます。
dispatch(changeKeyword(event.target.value));
Redux Toolkitを使うと、action typeやaction creatorを手書きする量は減ります。
ただし、基本の流れは変わりません。
dispatch(action)
↓
reducerがstateを更新
↓
storeに保存
↓
selectorで取得
↓
再描画
Redux Toolkitを使っていても、既存コードを読むときは、次の観点を追うことが大切です。
どのaction creatorをdispatchしているか
どのslice reducerで処理されているか
どのstateが更新されるか
非同期処理がある場合
実際のReduxコードでは、API通信などの非同期処理が入ることもあります。
たとえば、検索条件に応じて選択肢を取得する処理です。
const fetchOptions =
(keyword: string) => async (dispatch) => {
dispatch({
type: 'options/fetchStarted',
});
const response = await fetch(`/api/options?keyword=${keyword}`);
const options = await response.json();
dispatch({
type: 'options/fetchSucceeded',
payload: options,
});
};
非同期処理では、処理の開始時と完了時で別々のactionをdispatchすることがあります。
fetchStarted
↓
API通信
↓
fetchSucceeded
このような場合も、基本は同じです。
どこでdispatchしているか
どんなactionをdispatchしているか
reducerでどのstateを更新しているか
を追います。
ただし、非同期処理では await の間に別の操作が発生することがあります。
そのため、次のような観点も重要になります。
処理対象のIDを持っているか
古いレスポンスで上書きされないか
loading状態をどこで管理しているか
error状態をどこで管理しているか
このあたりは少し応用的な内容になるため、別の記事で整理したいと思います。
mapStateToPropsの場合
既存コードでは、useSelector ではなく mapStateToProps が使われていることもあります。
const mapStateToProps = (state: RootState) => ({
keyword: state.searchForm.keyword,
});
この場合も考え方は同じです。
mapStateToProps は、Redux storeのstateから必要な値を取り出し、コンポーネントのpropsとして渡します。
state.searchForm.keyword
↓
props.keyword
↓
componentで表示
useSelector でも mapStateToProps でも、見るポイントは変わりません。
このコンポーネントはRedux storeのどの値を見ているか
を確認します。
既存コードを読むときのチェックリスト
Reduxの既存コードを読むときは、次の観点で確認すると整理しやすいです。
表示している値は何か
その値はuseSelector/mapStateToPropsのどこから来ているか
selectorはstateのどこを参照しているか
どの操作でdispatchしているか
dispatchしているaction creatorは何か
actionのtypeとpayloadは何か
reducerでどのstateを更新しているか
更新後のstateをどのコンポーネントが参照しているか
特に大事なのは、次の2つです。
この値はどこから来ているのか
この値はどこで更新されるのか
この2つを追えるようになると、Reduxの既存コードはかなり読みやすくなります。
まとめ
今回は、Reduxを使った既存コードを読むときに、どの順番で追えばよいのかを整理しました。
Reduxのコードを読むときは、まず1つの値に絞るのがおすすめです。
そして、次の順番で追います。
1. 画面で使われている値を見る
2. selectorを見る
3. dispatchしている箇所を見る
4. action creatorを見る
5. reducerを見る
6. state更新の流れを一本につなげる
Reduxは、ファイルが分かれているため最初は複雑に見えます。
しかし、1つの値について、次の流れを追うと、少しずつ全体像が見えてきます。
表示
↓
selector
↓
dispatch
↓
action
↓
reducer
↓
state更新
↓
再描画
次回は、ReduxとJotaiを比較しながら、状態管理ライブラリごとの考え方の違いを整理します。