Mitomex Blog

Redux について(実践編)

2020-05-27

React Redux

前回の記事で Redux の概念について説明しました。

この記事では Redux の概念をもとに、React.js を使って 実際のコードではどのように表現していくのかみていきます。

React Project 作成

ローカルに Create React App を使って React をインストールするため、 ターミナルで以下のコマンドを実行します。

npx create-react-app my-redux-app

インストールが終わったら、プロジェクトのディレクトリに移動して yarn start を実行し、 アプリが正常に動くか確認します。

cd my-redux-app
yarn start

http://localhost:3000 にアクセスして以下のように表示されれば正常にインストールされています。

First React App

create-react-app を使ってインストールすると、制作が始めやすいように複数のファイルが用意されています。 ただし今回はこのファイルを使わないので /src ディレクトリにあるファイルすべて削除します。

cd src
git rm *
cd ..
mkdir src

React のインストールが終わったので、Redux による State 管理を追加していきます。

概念編でみてきた順番で実践編もみていきたいと思います。

State

Todo アプリで実装したい State の形は以下のようなオブジェクトです。

{
  todos:[
    {
      text: "筋トレをする",
      completed: false
    },
    {
      text: "買物に行く",
      completed: false
    }
  ]
}

上記はすでにタスクが追加された状態です。では State の初期状態はというとタスクが無い状態になります。

{
  todos: []
}

こちらが State の初期の状態です。この状態をファイルに記述しましょう。

src/initialState.js ファイルを作成し、以下のコードを記述します。

export const initialState = {
  todos: []
};

つづいて Action をみていきます。

Action

Action はやりたことを定義します。ここでは「タスクを追加する」という Action を実装していきます。

Action はオブジェクトで表現します。

{
  type: 'ADD_TODO',
  text: '筋トレをする'
}

type が「やりたいこと」で、text がタスクを追加するときに必要な「タスク名」です。

実装する際は Action Creator というものを使います。 src/actions.js ファイルを作成し、以下のコードを記述します。

export const addTodo = text => {
  return {
    type: 'ADD_TODO',
    text                // `text: text` の省略記法
  }
};

返しているものは { type: 'ADD_TODO', text: text' } という Action です。Action を作って返しているのでまさに Action Creator ですね。

また Action の type は Reducer の Switch 文でも使うので、使い回しできるように別途定義しておきます。

export const ADD_TODO = 'ADD_TODO';

export const addTodo = text => {
  return {
    type: ADD_TODO,
    text
  }
};

State と Action を実装したので、次は Reducer をみていきます。

Reducer

Reducer は State の値を変更する処理を定義するところです。

現在の State と Action を受け取って、新しい State を返す関数です。式で表すと

(nowState, action) => newState;

こんな関数になります。

Todo アプリの「タスクを追加する」ときにどのように実装するか、みていきましょう。 一番最初の形は以下のようになります。

src/reducers.js ファイルを作成し、以下のコードを記述します。

const todoApp = (state, action) => {
  return state;
};

上のコードでは State が変わっていませんが、State と Action を受け取って State を返すという形を作りました。

一番はじめの State は、State の章で定義した initialState になります。 引数 state のデフォルト値として initialState を設定しておきましょう。

const initialState = {
  todo: []
};

const todoApp = (state = initailState, action) => {
  return state;
};

次に「タスクを追加する」ときの State を変化させるための処理を追加していきます。

「タスクを追加する」ときは action で受け取った text 名のタスクを todos の配列に追加することになります。 コードにすると以下のようになります。

const initialState = {
  todo: []
};

const todoApp = (state = initailState, action) => {
  return {
    ...state,
    todos: [
      ...state.todos,
      {
        text: action.text,
        completed: false
      }
    ]
  };
};

ここでは Object の spread 構文でのマージと配列の spred 構文でのマージを使って、新しい State を返しています。

最後に Action は複数存在するので action.type によって処理を分岐させます。 また action.type 名を使い回せるように src/actions.js で変数として定義しているので、そちらを使います。

import { ADD_TODO } from './actions';

const initialState = {
  todo: []
};

const todoApp = (state = initialState, action) => {
  switch (action.type) {
    case ADD_TODO:
      return {
        ...state,
        todos: [
          {
            ...state.todos,
            text: action.text,
            completed: false
          }
        ]
      };
    default:
      return state;
  }
};

条件分岐に Switch 文を使いました。default の場合はそのまま state を返すようにしています。

ここまでで State、Action、Reducer についてみてきました。次は Store についてみていきましょう。

Store

Store は今までみてきた State Action Reducer をまとめるものになります。

Store を使うために Redux をインストールします。

yarn add redux

Store は Redux の createStore() メソッドに Reducer を渡して作成します。

src/store.js ファイルを作成し、以下のコードを記述します。

import { createStore } from 'redux';
import { todoApp } from './reducers';

const store = createStore(todoApp);

これで Redux による State 管理に必要なことは実装できました。

Dispatch

Store のデータを呼び出せるか確認してみましょう。

src/index.js ファイルを作成し、以下のコードを記述します。

import React from 'react';
import { render } from 'react-dom';
import { store } from "./store";

console.log(store.getState());

render(
  <div />,
  document.getElementById('root')
);

View に関してはまだ実装していません。仮に空の div があるだけです。 5行目に console.log(store.getState()) で現在の State は

この状態でターミナルから以下のコマンドを実行して、ブラウザのコンソール画面で出力を確認します。

yarn start

ブラウザのコンソールを確認してみると、

Store の getState() が出力したコンソール画面

todos が空の配列で取得できているのがわかります。

続いて State を更新する、「タスクを追加する」を確認してみましょう。

src/index.js ファイルの内容を以下のように変更します。

import React from 'react';
import { render } from 'react-dom';
import { addTodo } from "./actions"; // action をインポート
import { store } from "./store";

console.log(store.getState());  // 初期の State を確認
store.dispatch(addTodo('筋トレをする')); // タスクを追加
console.log(store.getState()); // 追加後の State を確認

render(
  <div />,
  document.getElementById('root')
);

Dispatch(addTodo()をやってみた結果)

store.dispatch(addTodo('筋トレをする')) でタスクを追加しました。 その結果、State が更新されて新しい State にタスクが追加されていることがわかります。

State が更新されるたびに getState() で確認するのは面倒なので 更新を監視するために subscribe() を追加します。

import React from 'react';
import { render } from 'react-dom';
import { addTodo } from "./actions";
import { store } from "./store";

console.log(store.getState());

const unscribe = store.subscribe(() => console.log(store.getState()));

store.dispatch(addTodo('筋トレをする'));

render(
  <div />,
  document.getElementById('root')
);

unscribe();

State が更新されると subscribe() が実行されます。 そのため dispatch() 後に再度 console.log(store.getState()) を行わなくても コンソールに最新の State が表示されます。

最後の行で subscribe() メソッドの返り値である unscribe() を実行して subscribe を解除しています。

Store を使って State を更新することができるようになりました。

ここから React のコンポーネント内で Redux を使えるようにしていきます。

React

追加したタスクが ul > li で表示させるようにします。

import { render } from 'react-dom';
import { addTodo } from "./actions";
import { store } from "./store";

console.log(store.getState());

const unscribe = store.subscribe(() => console.log(store.getState()));

store.dispatch(addTodo('筋トレをする'));
store.dispatch(addTodo('買物に行く'));

render(
  <ul>
    <li>筋トレをする</li>
    <li>買物に行く</li>
  </ul>,
  document.getElementById('root')
);

unscribe();

コンポーネントに直接タスクを記述するとこんな感じになります。

わかりやすく App コンポーネントに分けます。

import React from 'react';
import { render } from 'react-dom';
import { addTodo } from "./actions";
import { store } from "./store";

console.log(store.getState());

const unscribe = store.subscribe(() => console.log(store.getState()));

store.dispatch(addTodo('筋トレをする'));
store.dispatch(addTodo('買物に行く'));

const App = () => (
  <ul>
    <li>筋トレをする</li>
    <li>買物に行く</li>
  </ul>
);

render(
  <App />, // `App` コンポーネントに変更
  document.getElementById('root')
);

unscribe();

さらに

  • タスクのリストコンポーネント
  • ひとつのタスクコンポーネント

に分けます。

import React from 'react';
import { render } from 'react-dom';
import { addTodo } from "./actions";
import { store } from "./store";

console.log(store.getState());

const unscribe = store.subscribe(() => console.log(store.getState()));

store.dispatch(addTodo('筋トレをする'));
store.dispatch(addTodo('買物に行く'));

const Todo = ({ text }) => (
  <li>
    {text}
  </li>
);

const TodoList = () => (
  <ul>
    <Todo text="筋トレをする" />  // Todo コンポーネントに変更
    <Todo text="買物に行く" />  // Todo コンポーネントに変更
  </ul>
);

const App = () => (
  <TodoList />
);

render(
  <App />,
  document.getElementById('root')
);

unscribe();

タスクの名前を text プロパティで渡し、 Todo コンポーネントで分割代入(Destructing assignment)を使って受け取っています。

タスクのデータがハードコーディングされたままなので、変数 initialState に保存して map() メソッドで展開します。

import React from 'react';
import { render } from 'react-dom';
import { addTodo } from "./actions";
import { store } from "./store";

console.log(store.getState());

const unscribe = store.subscribe(() => console.log(store.getState()));

store.dispatch(addTodo('筋トレをする'));
store.dispatch(addTodo('買物に行く'));

const initialState = {
  todos: [
    {
      text: "筋トレをする",
      completed: false
    },
    {
      text: "買物に行く",
      completed: false
    }
  ]
};

const Todo = ({ text }) => (
  <li>
    {text}
  </li>
);

const TodoList = ({ todos }) => (
  <ul>
    {todos.map((todo, index) => (
      <Todo key={index} text={todo.text} />
    ))}
  </ul>
);

const App = () => (
  <TodoList todos={initialState.todos} />
);

render(
  <App />,
  document.getElementById('root')
);

unscribe();

TodoList コンポーネント内で Todo コンポーネントをループ処理で作成しています。

複数のコンポーネントがひとつのファイルに混在しているので、見やすくするためのそれぞれのコンポーネントを個別のファイルにします。

Todo.js ファイルを作成し、以下のコードを記述します。

import React from "react";

export const Todo = ({ text }) => (
  <li>
    {text}
  </li>
);

TodoList.js ファイルを作成し、以下のコードを追加します。

import React from "react";
import { Todo } from "./Todo";

export const TodoList = ({ todos }) => (
  <ul>
    {todos.map((todo, index) => (
      <Todo key={index} text={todo.text} />
    ))}
  </ul>
);

App.js ファイルを作成し、以下のコードを記述します。

import React from "react";
import { TodoList } from "./TodoList";

const initialState = {
  todos: [
    {
      text: "筋トレをする",
      completed: false
    },
    {
      text: "買物に行く",
      completed: false
    }
  ]
};

export const App = () => (
  <TodoList todos={initialState.todos} />
);

Todo.jsTodoList.jsApp.js に分けたので元の src/index.js ファイルは以下のようになります。

import React from 'react';
import { render } from 'react-dom';
import { addTodo } from "./actions";
import { store } from "./store";

import { App } from "./components/App";

console.log(store.getState());

const unscribe = store.subscribe(() => console.log(store.getState()));

store.dispatch(addTodo('筋トレをする'));
store.dispatch(addTodo('買物に行く'));

render(
  <App />,
  document.getElementById('root')
);

unscribe();

ここから App.js 内にある initialState を Store の State の値を使って表示できるように react-redux を使って変更していきます。

React With Redux

react-redux モジュールをインストールします。

yarn add react-redux

Store を React コンポーネントで使えるようにするために、 React Redux の Provider コンポーネントで <App /> を囲んで store を渡します。

src/index.js ファイルを以下のように変更します。

import React from 'react';
import { render } from 'react-dom';
import { addTodo } from "./actions";
import { store } from "./store";

// React Redux から `Provider` をインポート
import { Provider } from "react-redux";
import { App } from "./components/App";

console.log(store.getState());

const unscribe = store.subscribe(() => console.log(store.getState()));

store.dispatch(addTodo('筋トレをする'));
store.dispatch(addTodo('買物に行く'));

render(
  // <App /> を <Provider で囲む
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

unscribe();

これで Store が使えるようになったので、 State を使いたい TodoList コンポーネントに紐づけましょう。

直接 TodoList に紐づけてしまうと View とロジックが混ざってしまい見づらいので TodoListContainer コンポーネントを作り、このコンポーネントに Store を紐づけます。

コンポーネントと Store を紐づけるには React Redux の connect() を使います。

TodoListContainer.js を作成し、以下のコードを記述します。

import { connect } from "react-redux";
import { TodoList } from "../components/TodoList";

export const TodoListContainer = connect()(TodoList);

ここに Store データをどのような形でコンポーネントのプロパティと紐づけるかを決める必要があります。 mapStateToProps() メソッドを使って定義します。

TodoListContainer.js 内を以下のように変更します。

import { connect } from "react-redux";
import { TodoList } from "../components/TodoList";

const mapStateToProps = state => {
  console.log(state);
  return {
    todos: state.todos
  }
};

export const TodoListContainer = connect(mapStateToProps)(TodoList);

App コンポーネントで使用している TodoList を上記の TodoListContainer に変えるため、 App コンポーネントを以下のように変更します。

import React from "react";
import { TodoListContainer } from "../containers/TodoListContainer";

export const App = () => (
  <TodoListContainer />
);

これで Store の State を表示できるようになったので、今までダミーで使用していた initialState は必要なくなりました。 initialState を削除してもタスクが表示されています。

React Redux Connect

index.js ファイルの13行目の subscrib() メソッドで表示している console.log と TodoListContainer.js ファイルの5行目で表示している console.log の結果が同じであることも確認できます。

これで React コンポーネントに Store のデータを表示できるようになったので、index.js ファイルにある getState()subscribe() メソッドを削除します。 src/index.js を以下のように変更します。

import React from 'react';
import { render } from 'react-dom';
import { addTodo } from "./actions";
import { store } from "./store";

import { Provider } from "react-redux";
import { App } from "./components/App";

store.dispatch(addTodo('筋トレをする'));
store.dispatch(addTodo('買物に行く'));

render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

続いてアプリ内でタスクを追加できるようにしていきましょう。

まずタスクを追加するためのテキスト入力フィールドと追加ボタンがあるコンポーネントを作成します。

AddTodo.js ファイルを作成し、以下のコードを記述します。

import React from "react";

export const AddTodo = () => (
  <form>
    <input
      name="taskName"
    />
    <button type="submit">Add Todo</button>
  </form>
);

これを App コンポーネントで読み込み、表示させます。

import React from "react";
import { TodoListContainer } from "../containers/TodoListContainer";
import { AddTodo } from "./AddTodo";

export const App = () => (
  <React.Fragment>
    <TodoListContainer />
    <AddTodo />
  </React.Fragment>
);

見た目の部分が追加できたの、入力したタスク名が取得できるようします。 useState を使用して AddTodo.js を以下のように変更しましょう。

import React, { useState } from "react";

export const AddTodo = () => {
  const [taskName, setTaskName] = useState('');

  const handleSubmit = e => {
    e.preventDefault();
    setTaskName('');
  };

  const handleChange = ({ target }) => {
    setTaskName(target.value);
  };

  return (
    <form
      name="form"
      onSubmit={handleSubmit}
    >
      <p>
        {taskName}
      </p>
      <input
        name="taskName"
        value={taskName}
        onChange={handleChange}
      />
      <button type="submit">Add Todo</button>
    </form>
  );
};

useState を使用して taskName を設定し、input の onChange で値を取得できるようになりました。 また onSubmit で taskName が空になるようにも設定しました。

taskName の値を Store に追加できるようにします。

TodoListTodoListContainer を作成したように、 AddTodo のロジック部分を担う AddTodoContainer を作成します。

AddTodoContainer.js を作成し、以下のコードを記述します。

import { connect } from "react-redux";
import { AddTodo } from "../components/AddTodo";

export const AddTodoContainer = connect()(AddTodo);

AddTodoContainer.js で行うことは「タスクを追加する」で、「タスクを追加する」は Action になります。 なので AddTodoContainer.js が Store と紐づける必要があるのは Store の dispatch です。

dispatch をどのような形でコンポーネントのプロパティと紐づけるかを mapDispatchToProps() メソッドを使って定義します。

AddTodoContainer.js 内を以下のように変更します。

import { connect } from "react-redux";
import { AddTodo } from "../components/AddTodo";
import { addTodo } from "../actions";  // Action をインポートします

const mapDispatchToProps = { addTodo };

export const AddTodoContainer = connect(null, mapDispatchToProps)(AddTodo);

AddToContainer でつなげた addTodo を受け取って AddTodo コンポーネントで dispatch できるようにします。

AddTodo.js を以下のように変更します。

import React, { useState } from "react";

export const AddTodo = ({ addTodo }) => {
  const [taskName, setTaskName] = useState('');

  const handleSubmit = e => {
    e.preventDefault();
    addTodo(taskName);
    setTaskName('');
  };

  const handleChange = ({ target }) => {
    setTaskName(target.value);
  };

  return (
    <form
      name="form"
      onSubmit={handleSubmit}
    >
      <input
        name="taskName"
        value={taskName}
        onChange={handleChange}
      />
      <button type="submit">Add Todo</button>
    </form>
  );
};

Store をつなげた AddTodoContainer.js ができたので、 App コンポーネントでの呼び出しを変更します。

import React from "react";
import { TodoListContainer } from "../containers/TodoListContainer";
import { AddTodoContainer } from "../containers/AddTodoContainer";

export const App = () => (
  <React.Fragment>
    <TodoListContainer />
    <AddTodoContainer />
  </React.Fragment>
);

これでタスクを追加することができるアプリができました。タスク名を入力し、Add Todo ボタンをクリックしてタスクが追加されるのが確認できます。

src/index.js に確認のために追加していた dispatch() メソッドを削除しておきます。

import React from 'react';
import { render } from 'react-dom';
import { store } from "./store";

// React Redux から `Provider` をインポート
import { Provider } from "react-redux";

import { App } from "./components/App";

render(
  // <App /> を <Provider で囲む
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

また TodoListContainer.js にある console.log も削除しておきます。

まとめ

Todo アプリの

  • タスク一覧を表示する
  • タスクを追加する

という機能を実装していきながら react-redux モジュールの Providerconnect() を使って、 React コンポーネントと Redux の紐づけ方をまとめてみました。

参考ページ: Basic Tutorial | Redux