ducksというデザインパターンを使ってTodoListを作ってみた
ducks
というデザインパターンを使用して簡単なTodoアプリを作成しようと思います。
機能としては以下になります。
- タスクを追加できる
- タスクが終わったかどうかのラベルをつけることができる
環境構築
今回は自分みたいにブログの更新に時間がない方などが手軽にちょちょいと構築できるツールで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サイトでよく見かけるボタンやフォームなどの部品を爆速で作ることができます。
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; } }
actionとreducerを同じファイルに配置します。 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について
今回作成したTodoをgithubにあげたのでよかったらcloneして試してみたください。