React Redux勝手にチュートリアル(TODO List)

今回は基本的機能としてtodoの追加と削除ができるtodoリストを作成します。

前回と同様ジェネレーターからアプリのフォルダを自動生成するところから始めます。

詳しくは前回の記事参照。

uraway.hatenablog.com

  • まずは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のカウント