React Redux勝手にチュートリアル(TODO List)
今回は基本的機能としてtodoの追加と削除ができるtodoリストを作成します。
前回と同様ジェネレーターからアプリのフォルダを自動生成するところから始めます。
詳しくは前回の記事参照。
- まずは
index.js
js/index.js
import React from 'react'; import ReactDOM from 'react-dom'; import App from './containers/App'; import 'todomvc-app-css/index.css'; ReactDOM.render( <App />, document.getElementById('main') );
- メインとなるApp.jsの記述します。 大きくはインプットエリアのHeaderとtodoリストのMainSectionに分かれます。 コンポーネントを細かく分割することでテストも行いやすくなります。 todosを作り、 MainSectionに流します。
js/constainers/App.js
import React, { Component } from 'react'; import Header from '../components/Header'; import MainSection from '../components/MainSection'; const todos = [ { id: 0, text: 'Learn Redux' }, ]; class App extends Component { render() { return ( <div> <Header /> <MainSection todos={todos}/> </div> ); } } export default App;
- Headerコンポーネント
js/components/Header.js
import React, { Component, PropTypes } from 'react'; import TodoTextInput from './TodoTextInput'; class Header extends Component { render() { return ( <header className="header"> <h1>todos</h1> <TodoTextInput placeholder="What needs to be done?" /> </header> ); } } export default Header;
- TodoTextInputコンポーネント
js/components/TodoTextInput.js
import React, { Component, PropTypes } from 'react'; class TodoTextInput extends Component { render() { const { placeholder } = this.props; return ( <input placeholder={placeholder} type="text" autoFocuse /> ); } } export default TodoTextInput;
- MainSectionコンポーネント
js/components/MainSection.js
import React, { Component, PropTypes } from 'react'; import TodoItem from './TodoItem'; class MainSection extends Component { render() { const { todos } = this.props; return ( <section className="main"> <ul className="todo-list"> {todos.map(todo => <TodoItem key={todo.id} todo={todo} /> )} </ul> </section> ); } } export default MainSection;
- TodoItemコンポーネント
js/components/TodoItem.js
import React, { Component } from 'react'; import TodoTextInput from './TodoTextInput'; class TodoItem extends Component { render() { const { todo } = this.props; return ( <div className="view"> <label> {todo.text} </label> <button className="destroy" /> </div> ); } } export default TodoItem;
- Reduxを使用していきます。 アクションタイプを宣言しましょう。
js/constants/ActionTypes.js
export const ADD_TODO = 'ADD_TODO'; export const DELETE_TODO = 'DELETE_TODO';
- ADD_TODOとDELETE_TODOアクションを追加します。
js/actions/index.js
import * as types from '../constants/ActionTypes'; export function addTodo(text) { return { type: types.ADD_TODO, text }; }; export function deleteTodo(id) { return { type: types.DELETE_TODO, id }; };
- アクションごとのstateを指定します。
js/reducers/todo.js
import { ADD_TODO, DELETE_TODO } from '../constants/ActionTypes'; const initialState = [ { id: 0, text: 'Learn Redux', }, ]; export default function todos(state = initialState, action) { switch (action.type) { case ADD_TODO: return [ { id: new Date(), text: action.text, }, ...state, ]; case DELETE_TODO: return state.filter(todo => todo.id !== action.id ); default: return state; } }
- reducersをまとめます
js/reducers/index.js
import { combineReducers } from 'redux'; import todos from './todo'; const rootReducer = combineReducers({ todos, }); export default rootReducer;
- storeを生成する前準備です。
js/store/configureStore.js
import { createStore } from 'redux'; import rootReducer from '../reducers'; export default function configureStore(initialState) { const store = createStore(rootReducer, initialState); if (module.hot) { // Enable Webpack hot module replacement for reducers module.hot.accept('../reducers', () => { const nextReducer = require('../reducers').default; store.replaceReducer(nextReducer); }); } return store; }
Providerでstoreを流します。
js/index.js
import React from 'react'; import ReactDOM from 'react-dom'; import App from './containers/App'; import 'todomvc-app-css/index.css'; import { Provider } from 'react-redux'; import configureStore from './store/configureStore'; const store = configureStore(); ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById('main') );
- actionを発送します。 bindActionCreatorを併用することで自動的にマッピングされ、 dispatchされるので、 アクションを追加するたびにconnectにアクションを追加する必要がなくなります。
js/containers/App.js
import React, { Component, PropTypes } from 'react'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import Header from '../components/Header'; import MainSection from '../components/MainSection'; import * as TodoActions from '../actions'; class App extends Component { render() { const { todos, actions } = this.props; return ( <div> <Header addTodo={actions.addTodo} /> <MainSection todos={todos} actions={actions} /> </div> ); } } function mapStateToProps(state) { return { todos: state.todos, }; } function mapDispatchToProps(dispatch) { return { actions: bindActionCreators(TodoActions, dispatch), }; } export default connect( mapStateToProps, mapDispatchToProps )(App);
- addTodoを実施します
js/components/Header.js
import React, { Component, PropTypes } from 'react'; import TodoTextInput from './TodoTextInput'; class Header extends Component { handleSave(text) { if (text.length !== 0) { this.props.addTodo(text); } } render() { return ( <header className="header"> <h1>todos</h1> <TodoTextInput onSave={::this.handleSave} placeholder="What needs to be done?" /> </header> ); } } export default Header;
- 渡されたonSave関数を追加します
js/components/TodoTextInput.js
import React, { Component, PropTypes } from 'react'; class TodoTextInput extends Component { constructor(props) { super(props); this.state = ({ text: '' }); } handleSubmit(e) { const text = e.target.value.trim(); if (e.which === 13) { this.props.onSave(text); this.setState({ text: '' }); } } handleChange(e) { this.setState({ text: e.target.value }); } render() { const { placeholder } = this.props; const { text } = this.state; return ( <input placeholder={placeholder} type="text" autoFocuse value={text} onChange={::this.handleChange} onKeyDown={::this.handleSubmit} /> ); } } export default TodoTextInput;
- addTodoの動作を確認します
- 次にdeleteTodoの実装を行います。
js/components/MainSection
import React, { Component, PropTypes } from 'react'; import TodoItem from './TodoItem'; class MainSection extends Component { render() { const { todos, actions } = this.props; return ( <section className="main"> <ul className="todo-list"> {todos.map(todo => <TodoItem key={todo.id} todo={todo} {...actions} /> )} </ul> </section> ); } } export default MainSection;
- TodoItemコンポーネントからdeleteTodoは実行されます。
js/components/TodoItem.js
import React, { Component } from 'react'; import TodoTextInput from './TodoTextInput'; class TodoItem extends Component { render() { const { todo, deleteTodo } = this.props; return ( <li> <div className="view"> <label> {todo.text} </label> <button className="destroy" onClick={() => deleteTodo(todo.id)} >delete</button> </div> </li> ); } } export default TodoItem;
これでdelete機能も実装されました。
後はcssでお好みのデザインにどうぞ。
NEXT
- デバグツールの組み込み
- 編集機能
- done/undone機能
- done todoのフィルター
- done/undone todoのカウント