Reduxのコード解説
Reduxを使用したサンプルコードを用いて、実行流れと一緒に解説します。
Reduxの入門編としてお使いください。
準備
- 基本的なReactのコードは解説しませんので、多少読める必要があります。
- 解説順番の関係でReduxの用語が詳しい説明なしに使われている場合があります。その時は大体の理解で読み流すか、ここを簡単な用語集として使っても良いです。
- サンプルコードを見れる状態にしておいてください。
事前説明
このサンプルコードはwebpackを使用しています。webpackとは、簡潔言うと、複数のファイルを1つにまとめてくれるものです。
今回のサンプルではsrc/index.jsx
を起点として、import
で参照されているファイルやその先から参照されるファイルなどがbundle.js
という1つのファイルにまとめられkdist/
以下に配置されるようになっています。
もっと詳しい設定が見たい場合はwebpack.config.js
を見てください。
index.html
では<script src="./dist/bundle.js"></script>
というように、webpackで生成したbundle.js
を読み込んでいます。なのでindex.html
ではsrc/index.jsx
からアクセスされているファイル群の全てを読み込んでいるのと同じ状態になっています。
ここから説明するReact、Reduxのコード全てはsrc/
以下に格納されており、src/index.jsx
を起点として実行されます。
このサンプルコードは簡易的なToDoリストになっています。
ではここから本題のReduxの説明を始めていきます。
index.jsx
まずindex.jsxの先頭に大量のimport文がありますね。全部完全に理解する必要はありませんが、順番に説明していきましょう。
import * as React from "react";
import { render } from "react-dom";
import { Provider } from "react-redux";
import { createStore, applyMiddleware } from "redux";
import thunkMiddleware from "redux-thunk";
import { composeWithDevTools } from "redux-devtools-extension";
import { loadTodos } from "./actions";
import { rootReducer } from "./reducers";
import { App } from "./components/App";
const store = createStore(
rootReducer,
composeWithDevTools(applyMiddleware(thunkMiddleware))
);
const view = (
<Provider store={store}>
<App />
</Provider>
);
render(view, document.getElementById("root"));
store.dispatch(loadTodos())
まず分からないのは12行目にあるcreateStore
ですね。
この関数はReduxの仕組み上で重要なstoreを生成する関数です。storeはReduxの仕組み上で1つのみ絶対に生成する必要があるものです。
使い方はcreateStore(reducer, [stateの初期化値], 機能強化関数)
のように使えます。第2引数、第3引数は省略できます。今回は第2引数を省略しています。
const store = createStore(
rootReducer, // <- 外部ファイルで定義されているreducer
composeWithDevTools(applyMiddleware(thunkMiddleware))
);
// こういう書き方もできます
const store = createStore(
rootReducer,
initialState,
composeWithDevTools(applyMiddleware(thunkMiddleware))
)
rootReducer
とは、9行目のimport { rootReducer } from "./reducers";
で読み込んでいるfunction rootReducer
のことです。
export function rootReducer(state = { isLoading: false, tasks: [] }, action) { ... }
thunkMiddleware
とはactionの中で非同期関数を実行できるようにするためのミドルウェアです。それを適用するためにはapplyMiddleware
という関数で包む必要がある。
applyMiddleware
ではdispatch関数をラッピングし、Actionがreducerに到達する前にミドルウェアがキャッチできるようにするものです。
その前にあるcomposeWithDevTools
というのは少し特殊でReduxDevTool
というChromeの拡張プラグインを使うためのものです。これを使うとブラウザ上でReduxの内部の流れをデバックトレースできるという便利なものです。
コードの意味はDevToolミドルウェアをthunkMiddleware
の外側にくっつけるという意味になっています。
const store = createStore(
rootReducer,
composeWithDevTools(applyMiddleware(thunkMiddleware)) // <- 機能強化関数(enhancer)
);
Chrome拡張のReduxDevTool
次に17~22行目である以下の部分です。
まずProviderとはconnect()
されたコンポーネントの呼び出しで、Reduxのstoreを使用できるようにするものです。
現時点ではよく分からない単語などが出てきますが、とりあえずReduxの仕組みにstoreを載せる仕組みだと大まかに理解して先に進みましょう。
const view
は単純にReactのコンポーネントです。components/App.jsx
内のApp
コンポーネントを内部に埋め込んでいます。
view
コンポーネントの定義後、HTML要素の<div id="root"></div>
に対してview
コンポーネントを描画しています。
const view = (
<Provider store={store}>
<App />
</Provider>
);
render(view, document.getElementById("root"));
一番最後に書かれているコードの説明です。
store
というのは先ほどcreateStore()
で生成した変数です。dispatch()
にはActionCreatorを引数として実行します。
今回はactions.js
で定義した、const loadTodos
を実行しています。
-
ActionとはActionの識別子(
type: ""
)と任意の値で構成されたJavaScriptオブジェクトです。 - ActionCreatorとはActionを返す関数でdispatcherとも呼びます。
dispatchされたActionはReducerに渡されます。ここでいうReducerとはcreateStore()
に渡したrootReducer
のことです。
loadTodos
ActionCreatorはそのままActionを返しませんので少し特殊な関数になっています。これがthunkMiddleware
を追加したことによって書ける書き方です。
store.dispatch(loadTodos())
// actions.js
export const setTasks = tasks => ({ type: "SET_TASKS", tasks });
export const addTask = title => ({ type: "ADD_TASK", title });
export const loading = () => ({ type: "LOADING" });
export const loaded = () => ({ type: "LOADED" });
export const loadTodos = () => dispatch => {
dispatch(loading()); // ネットワーク通信をするため、ロードflagを立てる
setTimeout(() => {
fetch("http://192.168.33.10:8080/todos.json")
.then(res => res.json())
.then(json => {
dispatch(setTasks(json.tasks)); // APIから取得したTodosをセットする
dispatch(loaded()); // ロード終了
})
// eslint-disable-next-line
.catch(console.error);
}, 3000);
};
まず最初にdispatch(loading());
で別のActionCreatorをdispatchしています。
これによりtype: "LOADING"
のActionがReducerに渡されロード中flagが立ちます。
その後、APIからtodoの一覧を取得します。今回はテスト用データをローカルファイルとして用意しているため、setTimeout
でわざと3秒間の遅延をさせています。
取得したjsonは{ "tasks": [ ... ] }
という構成になっているので、json.tasks
をsetTasks
ActionCreatorに渡し、dispatchします。これによりstoreにtasksが追加されました。
ロードが終了したので、loaded
ActionCreatorをdispatchしています。
次はここで何度も出てきたReducerの中身を解説していこうと思います。
reducer.js
rootReducer(state, action)
ではstate引数の初期値を指定しています。 dispatchによって渡されるActionは、rootReducer()
の第2引数に渡されます。
関数内ではActionの識別子を元に処理を分岐しています。先ほど実行した"LOADING"
や"SET_TASKS"
、"LOADED"
などがありますね。
ここで返されたJavaScriptオブジェクトはstoreに渡され、stateが更新されます。
...state
というのはJavaScriptオブジェクト({ }
)であるstateを展開するというスプレッド演算子という記法です。
もし{ state, tasks: action.tasks }
と書いてしまった場合、{ { isLoading: false, tasks: [] }, tasks: [ ~ ]}
という形式になってしまいます。
スプレッド演算子で展開すると、{ isLoading: false, tasks: [], tasks: [ ~ ]}
という形式になり、同じkeyであるtasks
は後ろの値で上書きされます。
簡単に言うと、今までのstateの構造は壊さずに一部だけ更新している、ということですね。
※注: let lastId = 2;
というコードですが、ちゃんと書くのが面倒だったため書いた簡易的なものです。これが今のTasksの最大数だと仮定してください。
let lastId = 2;
export function rootReducer(state = { isLoading: false, tasks: [] }, action) {
switch (action.type) {
case "SET_TASKS":
return { ...state, tasks: action.tasks };
case "ADD_TASK":
return {
...state,
tasks: [...state.tasks, { id: lastId++, title: action.title }]
};
case "LOADING":
return { ...state, isLoading: true };
case "LOADED":
return { ...state, isLoading: false };
}
return state;
}
展開の細かい説明
ADD_TAKS
は少し特殊な書き方になっていますが、よく見て展開してみれば同じように解釈できます。
{ ...state, tasks: [
...state.tasks,
{ id: lastId++, title: action.title }
] }
上を展開すると、下のようになります。
{ isLoading: false, tasks: [
{ タスク1 },
{ タスク2 },
{ id: 次のタスクID,
title: Actionによって渡されてた追加タスクのtitle }
] }
components/App.jsx
まず今回見なくてもいい箇所をあげます。Typography
コンポーネントはMaterialUIというデザイン思考をライブラリにしたものの一部ですので、今回は見た目が変わるだけのものだと思っていてください。
App
コンポーネントはindex.jsx
からimportされ、Providerで挟まれて描画されますが、見た目としての基盤コンポーネントはApp
コンポーネントになっています。
ここではConnectedTodoItemList
とConnectedNewTodoItem
という2つのコンポーネントが描画されていますが、このコンポーネントはReduxの仕組みにのっとった特殊なコンポーネントです。
具体的な特殊コンポーネントの話は次の項目でしましょう。
import * as React from "react";
import Typography from "@material-ui/core/Typography";
import { ConnectedTodoItemList, ConnectedNewTodoItem } from "../containers";
export const App = () => (
<div>
<Typography variant="display1" gutterBottom={true}>
Todos
</Typography>
<ConnectedTodoItemList />
<ConnectedNewTodoItem />
</div>
);
container
前項で言った特殊なコンポーネントとは、具体的に言うとconnect()
を使ってラッピングされたコンポーネントのことです。
connect()
を使うと通常のコンポーネントはstoreの値を受け取れるようになり、stateやActionCreator(dispatcher)を与えることができます。
ここではアロー関数の挙動について知っておく必要があります。詳しくはこちらを見て、軽く理解しておいてください。
最初にconnect()
の説明からしていきます。
実際のコードではアロー関数を多く使っていますが、この説明コードでは一部のアロー関数を展開して、通常の関数として定義してわかりやすくしています。
細かい説明は大体コード内のコメントで書いたので、それを読めば大体コードの意味はわかるかと思います。
コンポーネントをconnect
する理由としては、そのコンポーネント内でActionをdispatchしたい場合、storeに格納されたstateを受け取りたい場合、などが考えられます。
このコードではstateとコンポーネントに対して渡されたpropsを全てコンポーネントに渡し、addTask
という関数を渡しています。
実際に渡したstoreの値はコンポーネント内でthis.props.*
という形でアクセスすることになります。
addTask
の場合、this.props.addTask
という形で呼び出します。
stateはpropsに展開されているので、this.props.state内の変数名
という形で呼び出せます。
// 引数にstateとコンポーネントのpropsを受け取ってJavaScriptオブジェクトを返す関数(第2引数は省略できる)
function stateToProps(state, props){
return { ...state, ...props }
}
// dispatchを引数に受け取ってActionCreatorをdispatchする関数
function dispatcherToProps(dispatch){
// title引数を受け取って、addTaskをdispatchする関数をアロー関数で定義している
// 関数をJavaScriptオブジェクトの中に入れている状態
return { addTask: title => dispatch(addTask(title)) }
}
const TestConnectedComponent = connect(
stateToProps,
dispatcherToProps
)(TodoComponent); // storeの値を受け取りたい通常のコンポーネント
本題のTodoアプリのcontainers.js
に戻りましょう。
ではまずConnectedTodoItemList
から見て見ましょう。
第1引数のstate => state
ですが、これは先ほど見た
引数にstateとコンポーネントのpropsを受け取ってJavaScriptオブジェクトを返す関数(第2引数は省略できる)
というものです。
第2引数は省略して、stateを受け取ってstateを返すだけの関数をアロー関数として定義して渡していますね。
コンポーネントはcomponents/TodoItemList.jsx
にあるTodoItemList
を渡していますね。
次にConnectedNewTodoItem
です。
こちらはTodoを追加するコンポーネントなので、いつぞやreducers.js
で見た"ADD_TASK"
が必要そうです。タスクを新しく追加する必要がありますからね。
第1引数は先ほどと同じstateをそのまま返すだけの関数ですね。
第2引数のdispatch => ({ addTask: title => dispatch(addTask(title)) })
は、先ほど見た
dispatchを引数に受け取ってActionCreatorをdispatchする関数
というものです。
関数内部ではaddTask
というkeyで、titleを受け取って新しいタスクを追加するaddTask
というActionCreatorがdispatchされています。
これはConnectedNewTodoItem
コンポーネント内で、Todoを追加するボタンが押された時にその内容をtitle引数としてthis.props.addTask(title)
を実行すると、title => dispatch(addTask(title))
という関数が実行されますね。
import { connect } from "react-redux";
import { TodoItemList } from "./components/TodoItemList";
import { NewTodoItem } from "./components/NewTodoItem";
import { addTask } from "./actions";
export const ConnectedTodoItemList = connect(state => state)(TodoItemList);
export const ConnectedNewTodoItem = connect(
state => state,
dispatch => ({ addTask: title => dispatch(addTask(title)) })
)(NewTodoItem);
components/{NewTodoItem.jsx, TodoItemList.jsx}
containers.js
でimportされていたNewTodoItem
やTodoItemList
も軽く見ていきましょう。
ここまで来ればReactの普通の知識と先ほどまでで出た内容がほとんどですのでもう少しです。
ここのコードは長いので一部だけ抜粋します。
constructor
でstateを定義していますが、これはstoreの持つstateとは全く別物です。
stateであることに代わりはありませんが、こちらのstateはこのコンポーネントが独自に持つstateです。
今回の用途としては、ユーザーが入力したタスクのタイトルを保存しておくためのものです。
onChangeなどの説明はしませんが、TextField
で変更されたタスクのタイトルをNewTodoItem
コンポーネントの内部stateで保存し、更新し続けます。
そして追加ボタンを押された時にコンポーネント内で定義されたregister()
を経由してthis.props.addTask(this.state.title);
を実行しています。
このaddTask
はcontainers.js
でConnectedNewTodoItem
コンポーネントに渡されたものです。
addTask
は内部でaddTask
ActionCreatorをdispatchしているので、Reducerで"ADD_TASK"
が実行されることでしょう。
"ADD_TASK"
が実行されると、this.state.title
を元に新しいタスクが作られstoreが持つstateのtasksに新しいタスクが追加されますね。
export class NewTodoItem extends React.Component {
constructor(props) {
super(props);
this.state = { title: "" };
}
setTitle(title) {
this.setState({
title: title
});
}
register() {
this.props.addTask(this.state.title);
}
render() {
return (
<div>
<TextField
id="title"
label="Title"
value={this.state.title}
onChange={e => this.setTitle(e.target.value)}
margin="normal"
/>
<Button
variant="contained"
color="primary"
onClick={() => this.register()}
>
ADD
</Button>
</div>
);
}
}
まとめ
どうでしょうか?
ここまででReduxの重要な部分の説明は全て終わりました。
おそらく一度では理解しきれない部分もあったと思いますが、Reduxの用語や概要がわかっている状態からならサンプルコードを元にすぐコードを書き始められるくらいはできるかもしれません。
コードを書き始めて理解を深めていくか、もう一度この記事を読み直して理解を深めるか、どちらにしても読み始める前と比べるてReduxについての理解が深まっていればこの記事を書いた価値はありました。
しかし、この記事では目に見える部分を重点的に解説しました。
Reduxを一度に全て理解するのはほぼ不可能です。
もっと深い内部のことを知りたければ、別の記事を読むのがいいでしょう。
この記事はただの入門編です。
簡単に説明するために、少し実際の挙動とは違う説明の仕方をしたかもしれませんがご了承ください。
よいReduxライフ(サイクル)を!