どうも、ぽっぽです。
GitHub – microsoft\frontend-bootcamp: Frontend Workshop from HTML\CSS\JS to TypeScript\React\Reduxを参考にReduxについて理解したことを紹介します。
Reactのルールに従うと、アプリケーションが複雑になればなるほどある問題に直面します。
- データは、コンポーネント間を
Props
を使って上のコンポーネントから下のコンポーネントに伝えます。中間に位置するコンポーネントでは使わないけど、更に下のコンポーネントで使う場合も中間のコンポーネントにProps
を渡す必要があります。この問題をprops drillingと言います。 - 共有データは、ユーザ操作やネットワークの更新によって変化します。コンポーネント間に変更を伝搬するのが困難になります。
この問題を解決する方法の一つにReduxがあります。
View
viewはデータとしてStoreを利用するReactコンポーネントです。
Actions
Actionは任意のイベントを表すJSONメッセージです。reducersを使って状態全体に影響します。必ずkeyにtype
を持ち、場合によって追加情報を含みます。
Store
storeはstate treeとdispatcherとreducersからなります。
- state treeはネストされたJSONです。ある状態から次の状態へreducersを使って更新します。
- dispatcherはactionを受け入れてそれらをreducersに通します。
- reducersは現在のstate treeとactionを取り込んで、state treeの次の状態を作成する関数です。これがstate treeを更新する唯一の方法です。
Reduxの使い方
Redux storeを作る
Reduxが提供するcreateStore()
関数でstoreを作成します。一般的にひとつのアプリケーションで1つのstoreを持ちます。引数にreducerと初期状態を指定します。
const store = createStore(reducer, initialState);
reducersを書く
単純なReduxでは、reducersは状態を変更する前に現在の状態をコピーしないとなりません。createReducer()
を使うと自動的にそれらを実施するので、状態の変化のみを書くだけですみます。
//第一引数はstateの状態
//第二引数はaction typeと一致するオブジェクト
const todosReducer = createReducer(
{},
{
addTodo: (state, action) => {
state[action.id] = { label: action.label, completed: false };
}
}
);
Dispatch action
actionをdispatchすることはactionと現在の状態がreducerに渡されます。getState()
でstoreの状態を見ることが出来ます。
const store = createStore(reducer, initialState);
store.dispatch({ type: 'addTodo', label: 'hello' });
store.dispatch({ type: 'addTodo', label: 'world' });
console.log(store.getState());
dispatchの引数はオブジェクトリテラルです。手で書くのは煩わしいのでactionを定義します。
const actions = {
addTodo: (label: string) => ({ type: "addTodo", label, id: nextId(), completed: false })
};
store.dispatch(actions.addTodo('hello'));
actions.addTodoは関数です。(label: string) => ({ type: "addTodo",label, id: nextId(), completed: false })
はアロー関数でオブジェクトリテラルを返します。
アロー関数でオブジェクトリテラル式を返す場合は、本体を丸括弧 () で囲みます。詳細はアロー関数を参照。
react redux
reactにreduxを適用するにはreactで作成したアプリケーションでstoreに接続する必要があります。
クラスコンポーネントはstoreにアクセスするには<Provider>
を使います。内部ではcontext APIを使って子コンポーネントにstoreを通します。
const store = createStore(reducers);
const App = () => {
return (
<Provider store={store}>
<div>Hello World!</div>
</Provider>
);
};
storeをPropsにマッピング
connect()
関数はstate treeの一部とdispatch関数をPropsにマッピングします。
import { connect } from 'react-redux';
const MyComponent = props => {
return <div>
{props.prop1}
<button onClick={props.action1()}>Click Me</button>
</div>;
};
const ConnectedComponent = connect(
state => ({
prop1: state.key1,
prop2: state.key2
}),
dispatch => ({
action1: (arg) => dispatch(actions.action1(arg)),
action2: (arg) => dispatch(actions.action2(arg)),
})
)(MyComponent);
<MyComponent>
はpropsを参照するシンプルはコンポーネントです。Reduxの知識は必要ありません。connect()
は引数に関数を指定しています。1つ目はstate treeの一部を<MyComponent>
のpropsにマッピングします。2つ目はdispatch関数を<MyComponent>
のpropsにマッピングします。connect()
は関数を返します。その関数は直ちに<MyComponent>
を<ConnectedCompnent>
にデコレートします。
こちらの関数の仕様で「入れ子の関数とクロージャ」の説明があります。関数が関数を返す場合の書き方があります。
function outside(x) {
function inside(y) {
return x + y;
}
return inside;
}
fn_inside = outside(3); // このように考えてください : 与えられたものに 3 を加算する関数を代入します
result = fn_inside(5); // 8 を返す
result1 = outside(3)(5); // 8 を返す
connect()
の最後でMyComponent
を指定しているのはこのためです。
また、引数に関数を指定することを高階関数と呼ぶようです。
こちらの第一級関数も参考になります
ReduxでTodoアプリを書いたソース
Reactで作成したTodoアプリにReduxを使ったソースをこちらからダウンロード出来ます。
このソースはCreate React App · Set up a modern web app by running one command.を使っています。複数のツールを組み合わせて必要な設定なども行ってくれているので便利でした。
npm install
npm start
Todoアプリを動作させる場合、NodeJSのコマンドプロンプトを開いて、ソースをダウンロードして展開した場所(package.json
ファイルがある)に移動します。1行目のコマンドを実行します。package.json
のパッケージをインストールして、インストールが終わるまで数十秒ほど待ちます。
2行目のコマンドを実行するとブラウザにTodoアプリが表示されます。
終わるときは、コマンドプロンプト上でCtrl + C
をタイプします。