React react-redux Todoリストで学ぶ 入門

ReactではReduxというStateの状態を管理するツールを利用するのが一般的になっているため、todoリストを元にreact-reduxについて学習していきたいと思います。

手を動かしたい方は、以下よりプロジェクトを作成ください。

スポンサーリンク

Reduxについて

Qiitaの方で詳細に説明されている記事がありましたので、こちらを参照ください。

Redux入門【ダイジェスト版】10分で理解するReduxの基礎
https://qiita.com/kitagawamac/items/49a1f03445b19cf407b7

Todoリスト作成してみよう

reduxのチュートリアルに沿って、Todoリストを作成します。
https://redux.js.org/basics/example

完成イメージ

ライブラリインストール

$ yarn add redux react-redux

・react-redux 7.1.1

・redux 4.0.4

Providerを設定しよう

index.js

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import rootReducer from './reducers'
import App from './components/App'
const store = createStore(rootReducer)
ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

登場人物が多くでてきましたので、それぞれ何をしているか見ていきましょう。

Provider:connect()関数でラップされたネストされたコンポーネントが、Reduxストアを利用にする
https://react-redux.js.org/api/provider

connect()関数:ReactコンポーネントをReduxストアに接続する役割
https://react-redux.js.org/api/connect

createStore:Reduxストアを作成する関数
https://redux.js.org/api/createstore

reducer:現在のstateとAction から、新しい state を生成する役割
今回は、reducersディレクトリ配下にtodoリストのActionに対応するstateを定義しています。
https://redux.js.org/basics/reducers

store:reducerの定義をまとめるオブジェクト
https://redux.js.org/basics/store

Action:アプリケーションからストアにデータを送信する情報のペイロード
https://redux.js.org/basics/actions

Providerの配下に定義された<App />でのAction(todoの追加・フィルタリングなど)をstoreに発信して、reducerで定義したActionに沿ってstateの情報を更新できるようにしているようです。

Actionの作成

actions/index.js

let nextTodoId = 0
export const addTodo = text => ({
  type: 'ADD_TODO',
  id: nextTodoId++,
  text
})
export const setVisibilityFilter = filter => ({
  type: 'SET_VISIBILITY_FILTER',
  filter
})
export const toggleTodo = id => ({
  type: 'TOGGLE_TODO',
  id
})
export const VisibilityFilters = {
  SHOW_ALL: 'SHOW_ALL',
  SHOW_COMPLETED: 'SHOW_COMPLETED',
  SHOW_ACTIVE: 'SHOW_ACTIVE'
}

Actionsでは、Todoリストで行う操作に関しての、定義をしています。

Todoの追加・Todoリストのフィルタリングなど。

Reducerの作成

reducers/todos.js

const todos = (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
  }
}
export default todos

Todoリストでは、Todoの追加・Todoのon/off(完了/未完了)のアクションがあるため、それぞれのActionに沿って、stateを返却しています。

reducers/visibilityFilter.js

import { VisibilityFilters } from '../actions'
const visibilityFilter = (state = VisibilityFilters.SHOW_ALL, action) => {
  switch (action.type) {
    case 'SET_VISIBILITY_FILTER':
      return action.filter
    default:
      return state
  }
}
export default visibilityFilter

Actionには全部表示・未完了のTodo表示・完了のTodo表示があるため、Todoリストのフィルタリングをしています。

reducers/index.js

import { combineReducers } from 'redux'
import todos from './todos'
import visibilityFilter from './visibilityFilter'
export default combineReducers({
  todos,
  visibilityFilter
})

combineReducers:すべての子reducer(todos/visibilityFilter)を呼び出し、その結果を単一の状態オブジェクトに収集しています。
https://redux.js.org/api/combinereducers

Componentの作成

components/App.js

import React from 'react'
import Footer from './Footer'
import AddTodo from '../containers/AddTodo'
import VisibleTodoList from '../containers/VisibleTodoList'
const App = () => (
  <div>
    <AddTodo />
    <VisibleTodoList />
    <Footer />
  </div>
)
export default App

各コンポーネントを定義して、アプリケーションの全体表示する。

components/Footer.js

import React from 'react'
import FilterLink from '../containers/FilterLink'
import { VisibilityFilters } from '../actions'
const Footer = () => (
  <div>
    <span>Show: </span>
    <FilterLink filter={VisibilityFilters.SHOW_ALL}>All</FilterLink>
    <FilterLink filter={VisibilityFilters.SHOW_ACTIVE}>Active</FilterLink>
    <FilterLink filter={VisibilityFilters.SHOW_COMPLETED}>Completed</FilterLink>
  </div>
)
export default Footer

Todoリストの全表示・未完了・完了のボタンを定義しています。

components/Link.js

import React from 'react'
import PropTypes from 'prop-types'
const Link = ({ active, children, onClick }) => (
  <button
    onClick={onClick}
    disabled={active}
    style={{
      marginLeft: '4px'
    }}
  >
    {children}
  </button>
)
Link.propTypes = {
  active: PropTypes.bool.isRequired,
  children: PropTypes.node.isRequired,
  onClick: PropTypes.func.isRequired
}
export default Link

Todoリストのフィルタリングボタン単体の表示などをしています。

components/Todo.js

import React from 'react'
import PropTypes from 'prop-types'
const Todo = ({ onClick, completed, text }) => (
  <li
    onClick={onClick}
    style={{
      textDecoration: completed ? 'line-through' : 'none'
    }}
  >
    {text}
  </li>
)
Todo.propTypes = {
  onClick: PropTypes.func.isRequired,
  completed: PropTypes.bool.isRequired,
  text: PropTypes.string.isRequired
}
export default Todo

Todo単体の表示をしています。

components/TodoList.js

import React from 'react'
import PropTypes from 'prop-types'
import Todo from './Todo'
const TodoList = ({ todos, toggleTodo }) => (
  <ul>
    {todos.map(todo => (
      <Todo key={todo.id} {...todo} onClick={() => toggleTodo(todo.id)} />
    ))}
  </ul>
)
TodoList.propTypes = {
  todos: PropTypes.arrayOf(
    PropTypes.shape({
      id: PropTypes.number.isRequired,
      completed: PropTypes.bool.isRequired,
      text: PropTypes.string.isRequired
    }).isRequired
  ).isRequired,
  toggleTodo: PropTypes.func.isRequired
}
export default TodoList

Todoリスト全体の表示・Todo単体のコンポーネントの親コンポーネントとなります。

Containerの作成

containers/AddTodo.js

import React from 'react'
import { connect } from 'react-redux'
import { addTodo } from '../actions'
const AddTodo = ({ dispatch }) => {
  let input
  return (
    <div>
      <form
        onSubmit={e => {
          e.preventDefault()
          if (!input.value.trim()) {
            return
          }
          dispatch(addTodo(input.value))
          input.value = ''
        }}
      >
        <input ref={node => (input = node)} />
        <button type="submit">Add Todo</button>
      </form>
    </div>
  )
}
export default connect()(AddTodo)

Todoを追加する際のform処理で、textボックスの内容をSubmitしています。

containers/FilterLink.js

import { connect } from 'react-redux'
import { setVisibilityFilter } from '../actions'
import Link from '../components/Link'
const mapStateToProps = (state, ownProps) => ({
  active: ownProps.filter === state.visibilityFilter
})
const mapDispatchToProps = (dispatch, ownProps) => ({
  onClick: () => dispatch(setVisibilityFilter(ownProps.filter))
})
export default connect(
  mapStateToProps,
  mapDispatchToProps
)(Link)

mapStateToProps:stateをPropsとして扱えるようにしています。

mapDispatchToProps:Action関数をPropsとして扱えるようにしています。

Link(Footerの子)コンポーネントでPropsを扱えるようにつなぎこんでいます。

containers/VisibleTodoList.js

import { connect } from 'react-redux'
import { toggleTodo } from '../actions'
import TodoList from '../components/TodoList'
import { VisibilityFilters } from '../actions'
const getVisibleTodos = (todos, filter) => {
  switch (filter) {
    case VisibilityFilters.SHOW_ALL:
      return todos
    case VisibilityFilters.SHOW_COMPLETED:
      return todos.filter(t => t.completed)
    case VisibilityFilters.SHOW_ACTIVE:
      return todos.filter(t => !t.completed)
    default:
      throw new Error('Unknown filter: ' + filter)
  }
}
const mapStateToProps = state => ({
  todos: getVisibleTodos(state.todos, state.visibilityFilter)
})
const mapDispatchToProps = dispatch => ({
  toggleTodo: id => dispatch(toggleTodo(id))
})
export default connect(
  mapStateToProps,
  mapDispatchToProps
)(TodoList)

Todoリストのコンポーネントでフィルター(全表示・完了表示・未完了表示)に応じたTodoを表示したいためのつなぎこみをしています。

ビルド

$ yarn start

ここまででTodoリストの作成が完了しました!

まとめ

Reduxの概念などを事前に把握していないと何をしているんだろう?と思う部分が結構ありました。

要所要所で分割して理解していくことで、react-reduxに関しての理解が深まると思いました。

役割がわかってくるとコードの見通しも良いので、アプリの規模に応じて取り入れてみるのもいいかと思います。