LoginSignup
9

More than 5 years have passed since last update.

Reduxをコードと一緒に学ぶ入門

Last updated at Posted at 2018-11-12

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からアクセスされているファイル群の全てを読み込んでいるのと同じ状態になっています。

ここから説明するReactReduxのコード全てはsrc/以下に格納されており、src/index.jsxを起点として実行されます。

このサンプルコードは簡易的なToDoリストになっています。

ではここから本題のReduxの説明を始めていきます。

index.jsx

まずindex.jsxの先頭に大量のimport文がありますね。全部完全に理解する必要はありませんが、順番に説明していきましょう。

index.jsx
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引数を省略しています。

index.jsx
const store = createStore(
    rootReducer, // <- 外部ファイルで定義されているreducer
    composeWithDevTools(applyMiddleware(thunkMiddleware))
);

// こういう書き方もできます
const store = createStore(
    rootReducer,
    initialState,
    composeWithDevTools(applyMiddleware(thunkMiddleware))
)

rootReducerとは、9行目のimport { rootReducer } from "./reducers";で読み込んでいるfunction rootReducerのことです。

reducer.js
export function rootReducer(state = { isLoading: false, tasks: [] }, action) { ... }

thunkMiddlewareとはactionの中で非同期関数を実行できるようにするためのミドルウェアです。それを適用するためにはapplyMiddlewareという関数で包む必要がある。

applyMiddlewareではdispatch関数をラッピングし、Actionがreducerに到達する前にミドルウェアがキャッチできるようにするものです。

その前にあるcomposeWithDevToolsというのは少し特殊でReduxDevToolというChromeの拡張プラグインを使うためのものです。これを使うとブラウザ上でReduxの内部の流れをデバックトレースできるという便利なものです。

コードの意味はDevToolミドルウェアをthunkMiddlewareの外側にくっつけるという意味になっています。

index.jsx
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コンポーネントを描画しています。

index.jsx
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を追加したことによって書ける書き方です。

index.jsx
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.taskssetTasks 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の最大数だと仮定してください。

reducers.js
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コンポーネントになっています。

ここではConnectedTodoItemListConnectedNewTodoItemという2つのコンポーネントが描画されていますが、このコンポーネントはReduxの仕組みにのっとった特殊なコンポーネントです。

具体的な特殊コンポーネントの話は次の項目でしましょう。

components/App.jsx
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))という関数が実行されますね。

containers.js
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されていたNewTodoItemTodoItemListも軽く見ていきましょう。

ここまで来ればReactの普通の知識と先ほどまでで出た内容がほとんどですのでもう少しです。

ここのコードは長いので一部だけ抜粋します。

constructorでstateを定義していますが、これはstoreの持つstateとは全く別物です。

stateであることに代わりはありませんが、こちらのstateはこのコンポーネントが独自に持つstateです。

今回の用途としては、ユーザーが入力したタスクのタイトルを保存しておくためのものです。

onChangeなどの説明はしませんが、TextFieldで変更されたタスクのタイトルをNewTodoItemコンポーネントの内部stateで保存し、更新し続けます。

そして追加ボタンを押された時にコンポーネント内で定義されたregister()を経由してthis.props.addTask(this.state.title);を実行しています。

このaddTaskcontainers.jsConnectedNewTodoItemコンポーネントに渡されたものです。

addTaskは内部でaddTask ActionCreatorをdispatchしているので、Reducerで"ADD_TASK"が実行されることでしょう。

"ADD_TASK"が実行されると、this.state.titleを元に新しいタスクが作られstoreが持つstateのtasksに新しいタスクが追加されますね。

components/NewTodoItem.jsx
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ライフ(サイクル)を!

参考

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9