React+Redux で Todoアプリを作ってみる

React+Redux で Todoアプリを作ってみる

ReactでTodoアプリを作ってみる

Reactのサンプルとして、Todoアプリを作ってみます。

テキストボックスで入力した値が、画面に追加される単純なものを作っていきます。

状態管理には Redux を使用します。

install

まずは必要なものをインストールします。

create-react-app

$ npm install -g create-react-app

Reactのプロジェクトは自力で構築するのは面倒なので、facebook製の create-react-app を使います。

create-react-app

$ create-react-app react-redux-todo

create-react-app でプロジェクトを作成します。少し時間がかかります。

Redux インストール

$ cd react-redux-todo
$ npm install --save redux react-redux redux-logger

Redux を使うに当たって必要な諸々をインストールします。プロジェクトのフォルダに移動してから実行します。

ディレクトリ構成

react-redux-todo
├── node_modules/
├── public/
├── package.json
├── README.md
└─┬ src/
  ├── App.js
  ├── index.js

こんな感じのディレクトリ構成でプロジェクトが作成されます。

Redux には 次の4つのパーツから構成されるので、それぞれをファイルに分割して管理するようにします。/src ディレクトリ以下にそれぞれのディレクトリを作成します。

  • actions
  • components
  • containers
  • reducers

actions/Todo.js, components/Todo.js, containers/Todo.js, reducers/Todo.js, createStore.js 各種ファイルをそれぞれ作成します。ひとまず空のファイルです。

すると /src 以下は次のようになります。

src/
  ├── App.js
  ├── index.js
  ├─┬ actions/
  │ └── Todo.js
  ├─┬ components/
  │ └── Todo.js
  ├─┬ containers/
  │ └── Todo.js
  ├─┬ reducers/
  │ └── Todo.js
  └── createStore.js

storeの作成

store は、状態を管理するグローバルで単一のオブジェクトです。まずはこれを作成する処理を作成します。

store の作成は、createStore.js で行います。ここでは createStore という関数を export します。

createStore.js

import { createStore as reduxCreateStore, applyMiddleware, combineReducers } from "redux";
import logger from "redux-logger";
import { todoReducer } from "./reducers/Todo";

export default function createStore() {
  const store = reduxCreateStore(
    combineReducers({
      todo: todoReducer,
    }),
    applyMiddleware(
      logger,
    )
  );

  return store;
}

Redux内にある同名の createStore という関数を内部で使用するので、as を使って “reduxCreateStore” という名前で import しています。

基本的には複数のReducerを利用することになるので、あらかじめ combineReducers を使って複数のReducerを使えるようにしています。

また、開発中は便利なので applyMiddleware で logger を適応しておくようにします。

仮のreducer作成

createStore.js で import している todoReducer を仮作成しておきます。state を受け取りそのまま返すだけです。

reducers/Todo.js

export const todoReducer = (state = {}) => state;

ReactにReduxを組み込む

index.jsを編集し、createStore で生成される store を React に組み込みます。

index.js

import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import registerServiceWorker from './registerServiceWorker';

import { Provider } from 'react-redux';
import createStore from './createStore';

const store = createStore();

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

registerServiceWorker();

やっていることは単純で、createStore で作成した store を Provider に渡し、rootコンポーネントにあたる <App> をラップしています。

ここまでで一度 npm start コマンドで実行してみます。正しく実装できていれば、エラーなくブラウザに画面が表示されます。とくに初期状態から何か変わったということはありませんが。

Todoコンポーネントを作成する

components/Todo.js

import React from 'react';

export default class Todo extends React.Component {
  render() {
    return (
      <div>
        <input type="text" />
        <button>追加</button><br />
        <ul>
          <li>TODO1</li>
          <li>TODO2</li>
        </ul>
      </div>
    );
  }
}

コンポーネントを components/Todo.js に定義します。ここではひとまずTodo追加用のテキストボックスとボタン、表示されるTodoリストはべた書きにしておきます。

定義したコンポーネントをルートコンポーネント内から呼び出して表示します。なお、元からあったタグは削除しています。

App.js

import React, { Component } from 'react';
import './App.css';
import Todo from './components/Todo';

class App extends Component {
  render() {
    return (
      <div className="App">
        <Todo />
      </div>
    );
  }
}

export default App;

表示して次のようになればOKです。

Actionを定義する

続けてActionを定義します。ひとまずTodoを追加するためのActionのみを定義します。

actions/Todo.js

export const addTodo = (todo) => {
  return { 
    type: 'ADD_TODO',
    payload: { todo: todo }
  };
}

Actionは、Reducerにしてほしい処理を伝えるためのメッセージです。単純なオブジェクトで、type プロパティを必ず持ち、これが処理のキーになります。

payload プロパティは、処理に使うパラメータで、この例では追加するTODOを持たせています。

Reducerの修正

定義したアクションをdispatchするための reducer を定義します。すでに仮のreducerを定義していますがこれを修正します。

reducers/Todo.js

const initialState = {
  todoList: []
}

export const todoReducer = (state = initialState, action) => {
  switch (action.type) {
    case 'ADD_TODO':
      // 新しく追加するTODO
      const todo = action.payload.todo;
      // stateを複製して追加
      const newState = Object.assign({}, state);
      newState.todoList.push(todo);
      return newState;
    default:
      return state;
  }
};

initialState は初期値です。今回は管理するTodoのリストのみを保持するオブジェクトとして定義します。

reducerは引数で現在の状態(state)とdispatchされたactionを受け取ります。受け取ったactionに応じた状態のstateを返すことで状態を更新します。

state は書き換えるのではなく新たなオブジェクトとしなければなりません。したがって単純に引数のstateに追加するのではなく、いったん Object.assign メソッドで複製(ディープコピー)した新たな state に対して追加し、それを戻り値とします。

containerを定義する

次にコンテナを定義します。コンテナはコンポーネントとreduxによる状態管理を結びつけます。

containers/Todo.js

import { connect } from 'react-redux';
import * as actions from '../actions/Todo';
import Todo from '../components/Todo';

const mapStateToProps = state => {
  return {
    todo: state.todo,
  }
}

const mapDispatchToProps = dispatch => {
  return {
    addTodo: (todo) => dispatch(actions.addTodo(todo)),
  }
}

export default connect(mapStateToProps, mapDispatchToProps)(Todo)

mapDispatchToProps 関数はこのコンポーネントで使用する state を切り出して、コンポーネント内のpropsで参照できるようにマッピングするための関数です。

mapDispatchToProps 関数は、dispatchするための関数をpropsにマッピングするための関数です。

それぞれ定義したものを、connect関数でコンポーネントに接続し、exportします。こうすることで、コンポーネントがreduxによる状態管理を意識せず、stateやdispatchを参照、実行できるようになります。

コンテナの使い方は簡単で、コンポーネントをimportしている箇所をコンテナに切り替えるだけです。

App.js

// import Todo from './components/Todo';
import Todo from './containers/Todo';

// 以下略

コンポーネントからstateを参照する

最後にコンポーネントを修正し、コンテナからマッピングされた state, dispatch用のpropsを参照するように変更します。

components/Todo.js

import React from 'react';

export default class Todo extends React.Component {
  state = {
    todo: ''
  }

  render() {
    console.log(this.props);

    // StoreのTodoからリストを生成
    const list = this.props.todo.todoList.map((todo, index) => <li key={index}>{todo}</li>)

    return (
      <div>
        <input type="text" onChange={elm => this.setState({ todo: elm.target.value })} />
        <button onClick={() => this.props.addTodo(this.state.todo)}>追加</button><br />
        <ul>
          {list}
        </ul>
      </div>
    );
  }
}

console.log(this.props) では、次のようになっているはずです。コンテナからマッピングされた state(todo) と dispatch用のメソッド(addTodo())が確認できます。

todoList.map((todo, index) => <li key={index}>{todo}</li>) では、現在のstateのtodoListからリストを作成しています。この例のように繰り返しで複製するjsxには key 属性で一意になる値を設定しないと警告がコンソール上に表示されます。

これを避けるためにindexを設定しています。

onChange={elm => this.setState({ todo: elm.target.value })} はテキストボックスの変更時のイベントで入力値をローカルのstateに保持しています。

onClick={() => this.props.addTodo(this.state.todo)} でボタン押下時にイベントでローカルのstateに保持されている入力値を追加用の関数に引き渡してdispatchすることで、storeの状態を書き換えます。

成功すると、状態が更新されて再描画されることで、リストに値が追加されていきます。この時の状態の遷移はコンソール上に表示されているはずです。

これは、redux-logger というミドルウェアを使っているためです。

完成

ここまでで一応の完成とします。

以上。

Javascriptカテゴリの最新記事