[前回] Django+Reactで学ぶプログラミング基礎(28): Reduxチュートリアル(準備と概要)
はじめに
前回は、チュートリアルの概要と、なぜReduxが必要か、
を勉強しました。
今回は、Reduxアプリの構成要素と、データフローの概要を、
サンプルを通して理解します。
今回の内容
- Reduxアプリの構成要素
- Reduxアプリのデータフロー
- Reduxコアをサンプルアプリで理解
Reduxアプリの構成要素
※ 引用元: Reduxアプリを介したデータフロー
-
Store
- すべてのReduxアプリの中心
- アプリのグローバル状態を保持するコンテナ
- いくつかの特別な機能を備えたJavaScriptオブジェクト
-
State/View(UI)/Reducer/Actions
- State
- アプリの状態を保持する唯一の手段
- 特定時点のアプリ状態を表す
- View
- 現在の状態に基づく、UIの宣言型説明
- UIは状態に基づきレンダリングされる
- Reducer
- storeで、reducer関数を実行し、新しいstateを計算
- Actions
- アプリで、ユーザー入力による、発生イベントおよび状態更新のトリガー
- ユーザーがボタンをクリックするなどアクションを起こすと
- stateが、発生内容に基づき更新される
- UI(view)は、新しい状態を読み取り、再レンダリングされる
- actionが、storeにdispatchされる
- storeが、reducer関数で新しいstateを計算
- State
Reduxアプリのデータフロー
※ 引用元: Reduxアプリを介したデータフロー
-
アプリでアクションが発生したら
- 発生イベントを表すプレーン(plain)なactionオブジェクトを作成
- actionをstoreにdispatchし、何が発生したか通知
- storeはroot reducer関数を実行し、古いstateとactionに基づき、新しいstateを計算
- storeはsubscriberにstateが更新されたことを通知
- view(UI)が、subscriberにより新しいデータで更新される
- storeはsubscriberにstateが更新されたことを通知
- storeはroot reducer関数を実行し、古いstateとactionに基づき、新しいstateを計算
- actionをstoreにdispatchし、何が発生したか通知
- 発生イベントを表すプレーン(plain)なactionオブジェクトを作成
-
上述フローが、storeに保持されているstateを変更する唯一の方法
- ※ storeに保持されたstateを直接変更してはいけない
Reduxコアをサンプルアプリで理解
環境準備
-
VS Codeで、
ファイル
メニューからフォルダーを開く...
アプリコード作成
-
src
フォルダーに新規ファイルindex.html
を追加
src/index.html
<!DOCTYPE html>
<html>
<head>
<title>Redux basic example</title>
<script src="https://unpkg.com/redux@latest/dist/redux.min.js"></script>
</head>
<body>
<div>
<p>
Clicked: <span id="value">0</span> times
<button id="increment">+</button>
<button id="decrement">-</button>
<button id="incrementIfOdd">Increment if odd</button>
<button id="incrementAsync">Increment async</button>
</p>
</div>
<script>
// Define an initial state value for the app
const initialState = {
value: 0
};
// Create a "reducer" function that determines what the new state
// should be when something happens in the app
function counterReducer(state = initialState, action) {
// Reducers usually look at the type of action that happened
// to decide how to update the state
switch (action.type) {
case "counter/incremented":
return { ...state, value: state.value + 1 };
case "counter/decremented":
return { ...state, value: state.value - 1 };
default:
// If the reducer doesn't care about this action type,
// return the existing state unchanged
return state;
}
}
// Create a new Redux store with the `createStore` function,
// and use the `counterReducer` for the update logic
const store = Redux.createStore(counterReducer);
// Our "user interface" is some text in a single HTML element
const valueEl = document.getElementById("value");
// Whenever the store state changes, update the UI by
// reading the latest store state and showing new data
function render() {
const state = store.getState();
valueEl.innerHTML = state.value.toString();
}
// Update the UI with the initial data
render();
// And subscribe to redraw whenever the data changes in the future
store.subscribe(render);
// Handle user inputs by "dispatching" action objects,
// which should describe "what happened" in the app
document
.getElementById("increment")
.addEventListener("click", function () {
store.dispatch({ type: "counter/incremented" });
});
document
.getElementById("decrement")
.addEventListener("click", function () {
store.dispatch({ type: "counter/decremented" });
});
document
.getElementById("incrementIfOdd")
.addEventListener("click", function () {
// We can write logic to decide what to do based on the state
if (store.getState().value % 2 !== 0) {
store.dispatch({ type: "counter/incremented" });
}
});
document
.getElementById("incrementAsync")
.addEventListener("click", function () {
// We can also write async logic that interacts with the store
setTimeout(function () {
store.dispatch({ type: "counter/incremented" });
}, 1000);
});
</script>
</body>
</html>
ブラウザで、カウンターアプリを使ってみる
-
F5を押してアプリを実行
-
ブラウザで、以下ボタンでさまざまなカウント操作を行ってみる
-
+
をクリック: 1増やす -
-
をクリック: 1減らす -
Increment if odd
をクリック: 奇数時のみ増やす -
Increment async
をクリック: 1秒遅延と伴う非同期加算
-
コード説明
State/Actions/Reducers
- まず、カウンターの状態を表すstateの初期値を定義
- Reduxアプリでstateの構成
- stateのroot部分として、JSオブジェクトを持つ
- サンプルでは、
initialState
- そのJSオブジェクト内に他の値が含まれる
- サンプルでは、
- stateのroot部分として、JSオブジェクトを持つ
- Reduxアプリでstateの構成
src/index.html
// Define an initial state value for the app
const initialState = {
value: 0
}
- 次に、reducer関数を定義
- reducerは、二つの引数を受け取る
-
state
: 現在の状態を表す -
action
: 何が起こったかを示す
-
- Reduxアプリ起動時、まだstateがないため
- reducerのデフォルト値として
initialState
を指定
- reducerのデフォルト値として
- actionオブジェクトのtypeフィールド
- actionの一意の名前を表すため、意味が分かりやすい名前に
- 例えば、
カウンターを増やす
actionのtype-
counter/incremented
と記述
-
- 例えば、
- actionのtypeに基づいて、返却されるstateが異なる
- stateが変更された場合、新しいstateオブジェクトを返す
- stateが変更されていない場合、既存のstateオブジェクトを返す
- ※ stateは、イミュータブルに更新
- 元のstateオブジェクトを直接変更せず、既存のstateをコピーしそれを更新
- ※ stateは、イミュータブルに更新
- actionの一意の名前を表すため、意味が分かりやすい名前に
- reducerは、二つの引数を受け取る
src/index.html
// Create a "reducer" function that determines what the new state
// should be when something happens in the app
function counterReducer(state = initialState, action) {
// Reducers usually look at the type of action that happened
// to decide how to update the state
switch (action.type) {
case 'counter/incremented':
return { ...state, value: state.value + 1 }
case 'counter/decremented':
return { ...state, value: state.value - 1 }
default:
// If the reducer doesn't care about this action type,
// return the existing state unchanged
return state
}
}
store
- Reduxライブラリの
createStore
APIを呼び出し、storeインスタンスを作成- reducer関数を
createStore
に渡す -
createStore
はreducer関数を使用し- 初期値を持つstateを生成
- 将来更新時、stateを計算
- reducer関数を
src/index.html
// Create a new Redux store with the `createStore` function,
// and use the `counterReducer` for the update logic
const store = Redux.createStore(counterReducer)
view(UI)
- UIは、画面に既存のstateを表示
- ユーザーがアクションを起こすと
- アプリはそのデータを更新してから、新しい値でUIを再レンダリング
-
store.getState()
メソッドを使用し、storeから最新stateを取得- その値を用いてUIを更新/表示
-
store.subscribe()
メソッドを呼び出し、storeが更新されるたびに呼び出されるsubscriberコールバック関数を渡す- サンプルでは、
render
関数をsubscriberとして渡している- storeが更新されるたびに、UIを最新の値で更新可能
- サンプルでは、
- Redux自体は、どこでも使用できるスタンドアロンのライブラリ
- 任意のUIレイヤーでReduxを使用可能
- ユーザーがアクションを起こすと
src/index.html
// Our "user interface" is some text in a single HTML element
const valueEl = document.getElementById('value')
// Whenever the store state changes, update the UI by
// reading the latest store state and showing new data
function render() {
const state = store.getState()
valueEl.innerHTML = state.value.toString()
}
// Update the UI with the initial data
render()
// And subscribe to redraw whenever the data changes in the future
store.subscribe(render)
actionをdispatch
- actionオブジェクトを作成し、storeにdispatchすることで、ユーザー入力に応答
-
store.dispatch(action)
を呼び出すと- storeはreducerを実行し、更新後のstateを計算
- subscriberを実行し、UIを更新
- サンプルでは、以下のような複数パターンでactionをdispatchし、reducerに計算させる
- 現在のカウンター値から加算または減算
- カウンター値が奇数の場合のみ加算
- 1秒遅延後に非同期に加算
-
src/index.html
// Handle user inputs by "dispatching" action objects,
// which should describe "what happened" in the app
document.getElementById('increment').addEventListener('click', function () {
store.dispatch({ type: 'counter/incremented' })
})
document.getElementById('decrement').addEventListener('click', function () {
store.dispatch({ type: 'counter/decremented' })
})
document
.getElementById('incrementIfOdd')
.addEventListener('click', function () {
// We can write logic to decide what to do based on the state
if (store.getState().value % 2 !== 0) {
store.dispatch({ type: 'counter/incremented' })
}
})
document
.getElementById('incrementAsync')
.addEventListener('click', function () {
// We can also write async logic that interacts with the store
setTimeout(function () {
store.dispatch({ type: 'counter/incremented' })
}, 1000)
})
おわりに
Reduxアプリの構成要素とデータフローを、
サンプルを通じて勉強しました。
次回も続きます。お楽しみに。