ducksというデザインパターンを使ってTodoListを作ってみた

ducksというデザインパターンを使用して簡単なTodoアプリを作成しようと思います。

f:id:top_men:20180820105137p:plain

機能としては以下になります。

  • タスクを追加できる
  • タスクが終わったかどうかのラベルをつけることができる

環境構築

今回は自分みたいにブログの更新に時間がない方などが手軽にちょちょいと構築できるツールでcreate-react-appを使用します。

以下のコマンドを実行します。

create-react-app ducks_todo

ducksについて

ducksの公式サイトには以下のようなルールが記載してあります。

  • reducerはexport deaultをしなければならない
  • action creatorは関数してexportしなければならない
  • actionは定数で定義する

などが挙げられます。

早速実際にアプリを作成していきましょう。

まずプロジェクトフォルダに移動してください。

cd ducks_todo

必要なパッケージをインストール

redux周りのモジュールをインストール

yarn add redux react-redux

次にmaterial-uiをインストールします。 reactに特化したマテリアルデザインCSSフレームワークです。webサイトでよく見かけるボタンやフォームなどの部品を爆速で作ることができます。

material-ui.com

yarn add @material-ui/core

moduleの作成

ducksの肝となる部分をまず初めに作っていきます。 src/modules以下にtodo.jsを作成します。

/* State */
let nextTodoId = 0;

/* Action */
const ADD_TODO = "ADD_TODO";
const TOGGLE_TODO = "TOGGLE_TODO";

/* Action Creator */
export const addTodo = text => {
  return {
    type: "ADD_TODO",
    id: nextTodoId++,
    text
  };
};
export const toggleTodo = id => {
  return {
    type: "TOGGLE_TODO",
    id
  };
};
/* Reducers */
export default function reducer(state = [], action) {
  switch (action.type) {
    case ADD_TODO:
      return [
        ...state,
        {
          id: action.id,
          text: action.text,
          completed: false
        }
      ];
    case TOGGLE_TODO:
      return state.map(
        todo =>
          todo.id === action.id ? { ...todo, completed: !todo.completed } : todo
      );
    default:
      return state;
  }
}

actionreducerを同じファイルに配置します。 ducksの公式サイトにも書かれていますが、actionとreducerは密結合な関係であるために同じファイル内にあると何かと見つけやすいですし、ファイルを跨いで確認しなくなるので個人的には良いなと思います。

componentsの作成

src直下にcomponents/Form.jsを作成してください。

import React from "react";
import Button from '@material-ui/core/Button';
export default class Form extends React.Component {
  render() {
    return (
    <form className = "addToto"
      onSubmit = {e => {
        e.preventDefault()
        if (!this.refs.inputText.value.trim()) {
          return
        }
        this.props.addTodo(this.refs.inputText.value)
        this.refs.inputText.value = ""
      }}
     >
      <input type = "text" className = "addToto_input" ref = "inputText" />
      <Button variant = "contained"
        color = "primary"
        style = {{marginLeft: "20px"}}
        onClick = {() => this.props.addTodo(this.refs.inputText.value)}
        >
        Submit
        </Button>
      </form>
    );
  }
}

次にTodoList、Todoコンポーネントを作成します。

import React from "react";
import Todo from "./Todo";
import Card from "@material-ui/core/Card";

export default class TodoList extends React.Component {
  render() {
    return (
      <Card className="todoList">
        <h2 className="todoList_title">TODO</h2>
        <div className="notCompleted">
          {this.props.todos.map(todo => (
            <Todo key={todo.id} toggleTodo={this.props.toggleTodo} {...todo} />
          ))}
        </div>
      </Card>
    );
  }
}
import React from "react";
import Card from "@material-ui/core/Card";
import Checkbox from "@material-ui/core/Checkbox";
import Typography from "@material-ui/core/Typography";

export default class Todo extends React.Component {
  render() {
    return (
      <Card className="todo">
        <div className="todo_header">
          <Checkbox
            tabIndex={-1}
            disableRipple
            onClick={() => this.props.toggleTodo(this.props.id)}
          />
          <Typography
            className={
              this.props.completed ? "completed todo_content" : "todo_content"
            }
          >
            {this.props.text}
          </Typography>
        </div>
        <div className="toggleBtn" />
      </Card>
    );
  }
}

こちらふんだんにmaterial-uiが用意してくれているコンポーネントを使用しています。本当に手軽で使いやすいです。

containerの作成

containerはstoreとcomponentを紐づけるための橋渡し役です。 container/AddTodo.jsを追加してください。

import { connect } from "react-redux";
import * as addTodoModule from "../modules/todo";
import Form from "../components/Form";

const mapDispatchToProps = dispatch => {
  return {
    addTodo: (text) => dispatch(addTodoModule.addTodo(text))
  };
};

export default connect(null, mapDispatchToProps)(Form);

FormコンポーネントにはActionしか利用しないのでconnectの第一引数はnullにするところがポイントです。

次にcontainer/VisibleTodo.jsを作成してください。

import { connect } from "react-redux";
import * as todoListModule from "../modules/todo";
import TodoList from "../components/TodoList";

const mapStateToProps = state => {
  return {
    todos: state.Todo
  };
};

const mapDispatchToProps = dispatch => {
  return {
    toggleTodo: (id) => dispatch(todoListModule.toggleTodo(id))
  };
};

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(TodoList);

TodoListには完了したかどうかのフラグを変更するためのtoggleTodoアクションと、追加したTodoのstateを渡します。

containerを配置するだけの役割のsrc/components/App.jsを作成します。

import React, { Component } from "react";
import "../App.css";
import AddTodo from "../containers/AddTodo";
import VisibleTodoList from "../containers/VisibleTodoList";

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

export default App;

その後ルートのコンポーネントである`index.jsを作成し、そこにApp.jsを配置します。

import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./components/App";
import { Provider } from "react-redux";
import configureStore from "./configureStore";

const store = configureStore();

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

AppコンポーネントをProviderでラップしてあげれば完成です。

まとめ

actionとreducerが同じファイル内にあるだけで探す手間が省け、actionのディレクトリが減るので個人的には使いやすいと感じました。 まだunduxというデザインパターンもあるので他のものを試してみたいと思います。

unduxについて

github.com

今回作成したTodoをgithubにあげたのでよかったらcloneして試してみたください。

github.com