Todoアプリを作っている記事はよくみるものの、それの一つ上の段階の記事がなかなかないと思ったので、書くことにしました。
React, Reduxを使ってTodoAppのtutorialを作った方を対象としています。
後編書きました -> React+Redux+Material-UI(+Redux Form)でReactTutorialの先へ行く[後編]
目的
redux-formとmaterial-uiを用いて、ある程度綺麗な見た目で、複数項目を入力できるTodoAppを作成する
この前半の記事では、redux-formを使ってロジックを実装するまでについて書いています。具体的には↓を作ります。

また、完成したコードはこちらにおいてあります。
[2018/8/27]
編集リクエストをいただきました
discription -> description に変更しました。
khskさんありがとうございます。
準備
雛形の作成と必要パッケージのインストール
% create-react-app todo-next
% cd todo-next
% npm install -S redux-form @material-ui/core classnames redux react-redux
% npm install -D redux-devtools
ディレクトリ構成
srcディレクトリ以下のみを触ります。
srcディレクトリの構成は以下です。
具体的には、actions, components, containers, reducersディレクトリを作成し、その下にファイルを配置し、必要のないファイルを削除しています。
% cd src && tree
.
├── actions
│ └── todos.js
├── components
│ └── todos.js
├── consts.js
├── containers
│ ├── todoForm.js
│ └── todos.js
├── index.js
├── logo.svg
├── reducers
│ └── todos.js
└── store.js
実装
まずは上に示したGIFの、追加機能のみがある(削除機能のない)ものを作っていきます。
src/index.js
registerServiceWorkerは今回は必要ないので消しています。
react-reduxのProviderを用いてstoreに反映させます。
import React from 'react';
import ReactDOM from 'react-dom';
import store from './store';
import { Provider } from 'react-redux';
import TodoApp from './containers/todos';
ReactDOM.render(
<Provider store={store}>
<TodoApp />
</Provider>,
document.getElementById('root')
);
src/consts.js
定数を設定していきます。今回はactionの名前を格納しました。
簡単な場合、例えば今回のようにADD_TODOが1つや2つ程度ならいいのですが、増えてきた場合や名前を変更したくなった際に、まとめておかないと直す箇所が多くなってしまうという問題が出てきます
export const actionNameList = {
addTodo: "ADD_TODO",
}
src/store.js
addTodoをするためのReducerと、redux-formがよしなにしてくれるReducerの2つが存在しているのでcombineReducersを使用しています。
また、process.env.~~の部分はReduxのdevツールを使用するために書いています。
これを使うことで、storeに対する動作や状態がブラウザから確認可能となります。
import { createStore, combineReducers } from 'redux';
import { reducer as reduxFormReducer } from 'redux-form';
import todoListReducer from './reducers/todos';
const reducer = combineReducers({
form: reduxFormReducer,
todoList: todoListReducer,
});
const store = createStore(
reducer,
process.env.NODE_ENV === 'development' && window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
export default store;
src/actions/todos.js
定数を呼び出し、typeに格納しています。
import { actionNameList } from '../consts';
export const addTodo = (todo) => ({
type: actionNameList.addTodo,
payload: {
todo
}
})
src/components/todos.js
idの管理などは一旦特にせず、reduxに依存しない部分のみを書きました。
引数のtodoListを受け取り、それを元にただ描画するためのコンポネントです。
import React from 'react';
export default class TodoList extends React.Component {
render() {
const { todoList } = this.props;
return (
<table>
<thead>
<tr>
<th>Title</th>
<th>Description</th>
</tr>
</thead>
<tbody>
{
todoList.map((todo, i) => (
<tr key={i}>
<td>{todo.title}</td>
<td>{todo.description}</td>
</tr>
)
)}
</tbody>
</table>
);
}
}
src/containers/todoForm.js
redux-form sampleを参考に書き進めました。
TitleとDescriptionを受け取る設定になっています。
redux-formはSubmit時のAction, Reducerの動作を代わりにやってくれるのでとても便利ですね。
(留意:redux-formのReducerの宣言はstore.jsでしました)
import React from 'react';
import { Field, reduxForm } from 'redux-form';
const TodoForm = props => {
const { handleSubmit, pristine, reset, submitting } = props;
return (
<form onSubmit={handleSubmit}>
<div>
<label>Todo Title</label>
<div>
<Field
name="title"
component="input"
type="text"
placeholder="題名"
/>
</div>
</div>
<div>
<label>Todo Description</label>
<div>
<Field
name="description"
component="input"
type="text"
placeholder="説明"
/>
</div>
</div>
<div>
<button type="submit" disabled={pristine || submitting}>送信</button>
<button type="button" disabled={pristine || submitting} onClick={reset}>
クリア
</button>
</div>
</form>
);
};
export default reduxForm({
form: 'todo',
})(TodoForm);
src/containers/todos.js
reduxの流儀にのっとり、storeからtodoListを引数にうけとり、それを元にしたTodoListコンポネントとredux-formを使ったTodoFormコンポネントを描画しています。
import React from 'react';
import { connect } from 'react-redux';
import { addTodo } from '../actions/todos';
import TodoForm from './todoForm';
import TodoList from '../components/todos';
class TodoApp extends React.Component {
render() {
return (
<div>
<h2>TodoList</h2>
<TodoForm onSubmit={this.props.addTodo}
/>
<hr />
<TodoList todoList={this.props.todoList} />
</div>
)
}
}
const mapStateToProps = (state) => ({
todoList: state.todoList,
});
const mapDispatchToProps = (dispatch) => ({
addTodo: (todo) => dispatch(addTodo(todo)),
deleteTodo: (index) => dispatch(deleteTodo(index)),
});
export default connect(mapStateToProps, mapDispatchToProps)(TodoApp);
src/reducers/todos.js
よくみた形だと思います、addTodoされた時に、既存のTodoListと新しく追加されたTodoを返り値として返します。
import { actionNameList } from '../consts';
const initialState = [];
export default function todoListReducer(state=initialState, action) {
switch (action.type) {
case actionNameList.addTodo:
return [...state, action.payload.todo];
default:
return state;
}
}
動作確認
無事、titleとdescriptionの2つを同時にSubmitすることができました。

削除の動作の実装
追加だけではなく、次は削除機能を追加してみます。
具体的には、『削除ボタンを押したタイミングでactionが発行され、todoList配列の中から削除ボタンを押されたものを削除し、描画する』という処理になります。
また、以下では...で省略を表しておりますので、あらかじめご了承ください。
consts.js
DELETE用のtypeの名前を定数として新しく格納します。
export const actionNameList = {
addTodo: "ADD_TODO",
deleteTodo: "DELETE_TODO",
}
reducers/todos.js
deleteのcaseを設定します。前までに書いてる箇所の省略には気をつけてください
...
case actionNameList.deleteTodo:
state.splice(action.payload.id, 1);
return [...state];
...
actions/todos.js
deleteTodoというアクションを追加します。addTodoとは違って引数にとっているのがtodoのデータではなくindexであることに注意してください。
...
export const deleteTodo = (index) => ({
type: actionNameList.deleteTodo,
payload: {
id: index,
}
})
components/todos.js
テーブル要素に操作カラムと削除ボタンを追加していきます。これをクリックした際に、引数としてうけとっているhandleDelete関数が呼ばれるという形式になっています。
...
<tr>
<th>Title</th>
<th>Description</th>
<th>操作</th>
</tr>
...
<tr key={i}>
<td>{todo.title}</td>
<td>{todo.description}</td>
<td>
<input type="button" value="削除" onClick={() => this.props.handleDelete(i)}/>
</td>
</tr>
...
containers/todos.js
deleteTodoを新たにimportして、todoListでの削除を可能にします。
また、handleDelete関数をbindしていますが、これがなければTypeError: Cannot read property 'deleteTodo' of undefinedが起こってしまいますので注意してください。
...
import { addTodo, deleteTodo } from '../actions/todos';
...
class TodoApp extends React.Component {
constructor(props) {
super(props)
this.handleDelete = this.handleDelete.bind(this)
}
handleDelete(index) {
this.props.deleteTodo(index)
}
render() {
return (
<div>
<h2>TodoList</h2>
<TodoForm onSubmit={this.props.addTodo}
/>
<hr />
<TodoList
todoList={this.props.todoList}
handleDelete={this.handleDelete}
/>
</div>
)
}
}
...
おわりに
TODOアプリだけではさらに何ができるのかがわかりにくく、また難しいものを見ると何が起こっているのかがわからないということがありそうだったので、React-Reduxを始める2歩目として読んでいただけるようなレベルを目指しましたが、いかがでしたでしょうか。
少し長くなってしまったので、material-uiの方は別記事にしようと思います。
読んでいただきありがとうございました。
また、もっと良い方法がある等ございましたらご教授いただけると幸いです。