[前回] Django+Reactで学ぶプログラミング基礎(29): Reduxチュートリアル(構成要素とデータフローの概要)
はじめに
前回は、Reduxの概念とデータフローの概要を勉強しました。
今回は、Reduxの概念とデータフローを深掘りします。
今回の内容
- バックグラウンドのコンセプト
- Reduxの用語
- コアコンセプトと原理
- Reduxアプリのデータフロー
バックグラウンドのコンセプト
状態管理
Reduxカウンターコンポーネントの例
- コンポーネントのstateとなるカウンターを追跡し
- ボタンがクリックされるたびにカウンターをインクリメント
- 自己完結型のアプリ(self-contained app)で、下記要素を含む
- state: アプリを動かす真実の源
- view: 現在のstateに基づくUIの宣言型説明
- actions: ユーザー入力により発生するイベント、またはstateを更新するトリガー
function Counter() {
// State: a counter value
const [counter, setCounter] = useState(0)
// Action: code that causes an update to the state when something happens
const increment = () => {
setCounter(prevCounter => prevCounter + 1)
}
// View: the UI definition
return (
<div>
Value: {counter} <button onClick={increment}>Increment</button>
</div>
)
}
一方向データフローの例
- ①state
- 特定の時点でのアプリの状態を表す
- ②view(UI)
- stateに基づき、レンダリング
- ③actions
- ユーザーのボタンクリックなどによりイベント発生すると、stateが更新される
- ④view(UI)
- UIは新しいstateに基づき、再レンダリングされる
※ 引用元: 一方向データフロー
複数コンポーネントで同じstateを共有したい
- 候補案: 親コンポーネントに
state
をリフトアップ(移管)する- 解決できる場合もあるが、必ずしも役立つとは限らない
- 解決策: コンポーネントから共有stateを抽出し、コンポーネントツリー外側の中央に配置
- コンポーネントツリーは大きな
view
となり、どのコンポーネントからも以下操作が可能- stateにアクセス
- actionをトリガー
- コンポーネントツリーは大きな
Reduxでstateの基本的な考え方
-
state管理の概念を定義し、viewから分離
- viewとstate間で独立性を維持
- コード構造化と保守性を向上
-
アプリのグローバルなstateを、同じ場所にまとめて保持
- state更新時に従うべきパターンをあらかじめ定義し
- コードを予測可能なものにする
- state更新時に従うべきパターンをあらかじめ定義し
イミュータビリティ(不変性)
- ミュータブルとは、変更可能であること
- JavaScriptオブジェクトと配列は、デフォルトで変更可能
- メモリ上で同じオブジェクト/配列への参照であっても、オブジェクトの内容を変更可能
const obj = { a: 1, b: 2 }
// オブジェクト自身を変えず、そのフィールドを変えられる
obj.b = 3
const arr = ['a', 'b']
// 配列自身を変えず、その要素を変えられる
arr.push('c')
arr[1] = 'd'
- イミュータブルとは、不変であること
- Reduxで、すべてのstate更新がイミュータブルに行われる必要あり
- 値をイミュータブルに更新するには、既存のオブジェクト/配列のコピーを作成し、そのコピーを変更
- 方法1: JavaScriptの配列/オブジェクトスプレッド演算子
- 方法2: 元配列の新しいコピーを返す配列メソッドを使用
const obj = {
a: {
// 安全にobj.a.cを更新するためには, オブジェクトのコピーが必要
c: 3
},
b: 2
}
const obj2 = {
// スプレッド演算子で、オブジェクトをコピー
...obj,
// aを書き換える
a: {
// obj.aをコピー
...obj.a,
// cを書き換える
c: 42
}
}
const arr = ['a', 'b']
// 配列の新しいコピーを作成し, 末尾に`c`を追加
const arr2 = arr.concat('c')
// または、元の配列のコピーを作成
const arr3 = arr.slice()
// コピーを書き換える(ミューテート)
arr3.push('c')
Reduxの用語
Actions
- アプリで発生したことを説明するイベント
- プレーンなJavaScriptオブジェクト
- typeフィールド
- actionを一意識別する、わかりやすい名前
- 通常、
domain/eventName
のような文字列で記述- 最初の
domain
は、acitonが属する機能またはカテゴリ - 2番目の
eventName
は、発生した特定イベント
- 最初の
- payloadフィールド
- 発生イベントの追加情報
const addTodoAction = {
type: 'todos/todoAdded',
payload: 'Buy milk'
}
Reducer
-
現在のstateとactionを受け取り、stateの更新方法を決定し、新しいstateを返す関数
(state、action) => newState
- 受信したaction(イベント)タイプに基づき、イベントを処理するイベントリスナーを決める
-
Reducer関数が従うべきルール
- stateの更新は、イミュータブルである必要あり
- stateとaction引数を受け取り、新しいstateを計算するのみ
- 既存のstateを変更してはいけない
- 既存のstateをコピーし、コピーされた値に変更を加える
- 非同期ロジックを実行してはいけない
- ランダムな値を計算してはいけない
- stateの更新は、イミュータブルである必要あり
-
Reducer関数内ロジックの手順
- reducerが対応すべきactionの場合
- stateのコピーを作成し、コピーを更新してから、返す
- それ以外の場合
- 既存のstateを変更せず、そのまま返す
- reducerが対応すべきactionの場合
const initialState = { value: 0 }
function counterReducer(state = initialState, action) {
// reducerがactionを処理すべきか
if (action.type === 'counter/incremented') {
// stateのコピーを作成
return {
...state,
// コピーを新しい値に更新
value: state.value + 1
}
}
// さもなければ、既存stateをそのまま返す
return state
}
Store
- Reduxアプリのstateは、storeオブジェクトに存在
-
configureStore
メソッドにreducerを渡し実行することで、storeを作成 -
getState
メソッドを用いて、現在のstateを返す
import { configureStore } from '@reduxjs/toolkit'
const store = configureStore({ reducer: counterReducer })
console.log(store.getState())
// {value: 0}
Dispatch
- stateを更新する唯一の手段は、
-
store.dispatch
メソッドにactionオブジェクトを渡し、実行すること
-
- storeは、reducer関数を実行し、新しいstate値を保存
-
getState()
メソッドを用いて、更新されたstate値を取得 - アプリでは、actionのdispatchを
イベントのトリガー
と見做すことができる- 何かイベントが起こったら、storeにそれを伝える
- reducerは、イベントリスナーのように振る舞う
- 関連するactionを受け取ったら、それに応じてstateを更新
store.dispatch({ type: 'counter/incremented' })
console.log(store.getState())
// {value: 1}
Selector
- storeのstate値から特定情報を抽出するための関数
- アプリが大きくなると、さまざまな部分で同じデータを読み取る必要が生じる
- 共通関数化により、同じロジックの繰り返しを回避
const selectCounterValue = state => state.value
const currentValue = selectCounterValue(store.getState())
console.log(currentValue)
// 2
コアコンセプトと原理
信頼できる唯一の情報源は、一ヵ所にまとまって保存されるstate
-
アプリのグローバルstateは、単一のstoreにオブジェクトとして保存される
- 特定のstateは、複数場所で複製されるのではなく、一つの場所にのみ存在する必要あり
-
これにより、状況の変化に応じてアプリの状態をデバッグ/調査しやすくなる
- アプリ全体とやり取りするために必要なロジックを一元化できる
-
アプリで、すべてのstateがstoreに保存されるとは限らない
- stateの一部がReduxに属するかUIコンポーネントに属するかを決定する必要あり
-
stateは読み取り専用
- stateを変更する唯一の方法は、何が起こったかを示すactionオブジェクトをdispatchすること
- メリット1: UIが誤ってデータを上書きすることを回避
- メリット2: stateの更新が発生した理由を簡単に追跡できる
- actionはプレーンなJSオブジェクトであるため
- ログに記録可能
- シリアル化して保存可能
- デバッグやテストの目的で再生可能
-
stateの変更は、純粋(Pure)なreducer関数により行われる
- reducer関数で、actionに基づきstateツリーを更新する方法を定義
- reducer関数は、前のstateとactionを取り、次のstateを返す純粋関数(Pure Function)
- reducerを小さな関数に分割し作業を支援したり
- 一般的なタスク用に再利用可能なreducerを作成可能
Reduxアプリのデータフロー
一方向データフロー
手順を詳細に分割
※ 引用元: Reduxのデータフロー
初期設定フェーズ
- storeは、root reducer関数を使用し、作成される
- storeはroot reducerを1回呼び出し、戻り値を初期状態として保存
- UIが最初にレンダリングされるとき、UIコンポーネントはRedux storeの現在stateにアクセス
- stateを使用し、何をレンダリングするか決定
- store更新をsubscribeしておくと、stateの更新を検知できる
更新フェーズ
- アプリで、ユーザーがボタンをクリックするなどイベント発生
- アプリコードは、actionをRedux storeにdispatch
dispatch({type: 'counter/incremented'})
- アプリコードは、actionをRedux storeにdispatch
- storeは、前のstateと現在のactionで、reducer関数を再度実行
- 戻り値を新しいstateとして保存
- storeは、subscribeされているUIに、store更新を通知
- storeからのstate値を必要とする各UIコンポーネントは、stateが変更されているか確認
- state更新を検知した各UIコンポーネントは、新しいstateで強制的に再レンダリングする
- 画面の表示内容が更新される
おわりに
Reduxの概念とデータフローを深掘りしました。
次回も続きます。お楽しみに。