DEV Community

kaede
kaede

Posted on • Edited on

React Redux Tutorial Part 4 -- connect API の mapStateToProps を使って Todo アプリを作る

要件

https://react-redux.js.org/tutorials/connect#connecting-the-components

今回は paylaod に引数を送るだけの action creaters と
動いた actionTypes によって payload から値を受け取ってグローバルステートに変化を与える reducers
これらがはっきり分かれている。

普通の?コンポーネントは

  • AddTodo が input の onChange から ADD_TODO の action を dispatch
  • TodoList が todo のリストを描画し、VisibilityFilters を内部で使用
  • Todo が todo ひとつを描画し、onClick で終わったかどうかのステータスをトグルする action を dispatch
  • VisibilityFilters が全て、完了、未完了、の条件でフィルターする。これらは activeFilter の引数で受け取り、setFilter を dispatch する。

という設計になっていて


Redux の store と reducer の設計

todo の話が難しいので、実際にコードを追ってみる



redux/actionTypes

src/ に redux/ のフォルダを作り、そこに

actionTypes.ts を作成する。

export const ADD_TODO = "ADD_TODO"; export const TOGGLE_TODO = "TOGGLE_TODO"; export const SET_FILTER = "SET_FILTER"; 
Enter fullscreen mode Exit fullscreen mode

必要なアクションは

  • やることの追加
  • やることのトグル(完了未完了の切り替え)
  • 表示するやることリストのフィルターのセット

これらの 3 つになる。
なのでこれら 3 つ文字列で定数として定義する。

これが actionTypes 。


redux/actions

次に今作った actionTypes を元にした actions を作成する

import { ADD_TODO, TOGGLE_TODO, SET_FILTER } from "./actionTypes"; 
Enter fullscreen mode Exit fullscreen mode

actionTypes を import する
これらの文字列の定数たちをアクションごとの types という識別名に使う。


addTodo

let nextTodoId = 0; export const addTodo = content => ({ type: ADD_TODO, payload: { id: ++nextTodoId, content } }); 
Enter fullscreen mode Exit fullscreen mode

次となる ID を 0 で初期化して

content を受け取り
paylaod に nextTodoId と 受け取った content を入れて
nextTodoId を +1 して
type を ADD_TODO にセットする

addTodo というアクションを作成。


toggleTodo

export const toggleTodo = id => ({ type: TOGGLE_TODO, payload: { id } }); 
Enter fullscreen mode Exit fullscreen mode

id を受け取り、payload に渡し、
type に TOGGLE_TODO をつける


setFilter

export const setFilter = filter => ({ type: SET_FILTER, payload: { filter } }); 
Enter fullscreen mode Exit fullscreen mode

filter を受け取り、paylaod に渡し、
SET_FILTER という type をつける



reducers/todos

reducers は何をするのか

https://react-redux.js.org/tutorials/connect#connecting-the-components

チュートリアルの解説の The Redux Store/Reducers の章を見ると
この reducers は ADD_TODO, TOGGLE_TODO, SET_FILTER, のアクションが動いた時に連動して動く。

そしてキープされるグローバルステートのオブジェクトには 2 種類ある。

  • byIDs が中身のある TODO リストで
  • allIds は TODO リストの ID だけ

と解釈する。

import { ADD_TODO, TOGGLE_TODO } from "../actionTypes"; 
Enter fullscreen mode Exit fullscreen mode

actionTypes から やることの追加とやることの切り替えの定数を import

const initialState = { allIds: [], byIds: {} }; 
Enter fullscreen mode Exit fullscreen mode

初期ステートの中に全ての ID の配列、個別の ID のオブジェクト
これらを空で定義する。
ここからグローバルステートが作られ、保持されていく。

export default function(state = initialState, action) { switch (action.type) { case ADD_TODO: { //... } case TOGGLE_TODO: { //... } default: // ... } } 
Enter fullscreen mode Exit fullscreen mode

そして このモジュールの機能として
ADD_TODO, TOGGLE_TODO, そして else として動く default
これらを定義する

アクションは payload に type と オブジェクトを送るだけだが、
reducer でその type の名前に応じたものが動き、オブジェクトの中身を処理する。


case ADD_TODO

ADD_TODO の時は

 case ADD_TODO: { const { id, content } = action.payload; return { ...state, allIds: [...state.allIds, id], byIds: { ...state.byIds, [id]: { content, completed: false } } }; } 
Enter fullscreen mode Exit fullscreen mode

allIds に過去全ての todos の ID たちを展開して、
現在を渡されている ID を付け加えて

byIds に byIds に入っている todos を展開して
現在 渡されている todo の ID を key とする
渡されたコンテントと、未完了を表す complete: false
を入れたオブジェクトを付け加えて

現在の byIds と allIds が入っているステートに今の 2 つを付け加えて
return で返す。

これが ADD_TODO が呼び出された時に動くことになる。


case TOGGLE_TODO

 case TOGGLE_TODO: { const { id } = action.payload; return { ...state, byIds: { ...state.byIds, [id]: { ...state.byIds[id], completed: !state.byIds[id].completed } } }; } 
Enter fullscreen mode Exit fullscreen mode

TOGGLE_TODO の時は

id, content を両方取ってきていた ADD の時と異なり
id のみを action.payload から取得する

現在のステートを展開し、それにこれらを付け加える。

byIds に 現在入っていた byIds の中身を展開し、それに

引数の ID を key として byIds の現在の ID 番号のステートを展開し
引数の ID の completed の True False を反転させる。


default

どちらでもなくて action が動いた時は、グローバルステートをそのまま返すと解釈する。



Filter 機能

  • Todo の追加
  • Todo の completed のトグル

これらと同じように
filter の変更でも reducer を別ファイルに作る。

constants.ts

export const VISIBILITY_FILTERS = { ALL: "all", COMPLETED: "completed", INCOMPLETE: "incomplete" }; 
Enter fullscreen mode Exit fullscreen mode

表示フィルターの定数の定義。自明。


reducers/visibilityFilters.js

import { SET_FILTER } from "../actionTypes"; import { VISIBILITY_FILTERS } from "../../constants"; 
Enter fullscreen mode Exit fullscreen mode

最初に作った actionTypes から SET_FILTER の定数を import
直前で作った constants から VISIBILITY_FILTERS の
定数オブジェクトを import

const initialState = VISIBILITY_FILTERS.ALL; 
Enter fullscreen mode Exit fullscreen mode

保持されるグローバルステートの初期値を all にする

const visibilityFilter = (state = initialState, action) => { switch (action.type) { case SET_FILTER: { return action.payload.filter; } default: { return state; } } }; 
Enter fullscreen mode Exit fullscreen mode

SET_FILTER が呼ばれた時に、action.payload の filter から
受け取った filter の値をそのまま返すシンプルな処理。

export default visibilityFilter; 
Enter fullscreen mode Exit fullscreen mode

そして export する。自明。

これで reducers は全て作れた。


reducers/index

import { combineReducers } from "redux"; import visibilityFilter from "./visibilityFilter"; import todos from "./todos"; export default combineReducers({ todos, visibilityFilter }); 
Enter fullscreen mode Exit fullscreen mode

最後に reducers/index で combineReducers を使って
まとめる。これで store と連携する準備が整った。



redux/selectors.js

redux 最後の大物。いくつもあるが、
getTodosByVisibilityFilter しか使われていないように見える。

import { VISIBILITY_FILTERS } from "../constants"; 
Enter fullscreen mode Exit fullscreen mode

フィルター定数を import して

export const getTodosByVisibilityFilter = (store, visibilityFilter) => { const allTodos = getTodos(store); switch (visibilityFilter) { case VISIBILITY_FILTERS.COMPLETED: return allTodos.filter(todo => todo.completed); case VISIBILITY_FILTERS.INCOMPLETE: return allTodos.filter(todo => !todo.completed); case VISIBILITY_FILTERS.ALL: default: return allTodos; } }; 
Enter fullscreen mode Exit fullscreen mode

しかしこれが
getTodos を使い

export const getTodos = store => getTodoList(store).map(id => getTodoById(store, id)); 
Enter fullscreen mode Exit fullscreen mode

getTodos が getTodoList と getTodoById を使い

export const getTodoList = store => getTodosState(store) ? getTodosState(store).allIds : []; export const getTodoById = (store, id) => getTodosState(store) ? { ...getTodosState(store).byIds[id], id } : {}; 
Enter fullscreen mode Exit fullscreen mode

getTodoList と getTodoById が getTodoState を使った。

export const getTodosState = store => store.todos; 
Enter fullscreen mode Exit fullscreen mode

結局、entire file を使うことになった。。。



components/TodoList

描画コンポーネントの TodoList で、selectors の

import { getTodosByVisibilityFilter } from "../redux/selectors"; const mapStateToProps = state => { const { visibilityFilter } = state; const todos = getTodosByVisibilityFilter(state, visibilityFilter); return { todos }; } 
Enter fullscreen mode Exit fullscreen mode

getTodosByVisibilityFilter を使う。todos を持ってくるために使った。

import { connect } from "react-redux"; export default connect(mapStateToProps)(TodoList); 
Enter fullscreen mode Exit fullscreen mode

これが connect されることによって
同じファイル内部の TodoList で使えるようになる。

import Todo from "./Todo"; const TodoList = ({ todos }) => ( <ul className="todo-list"> {todos && todos.length ? todos.map((todo, index) => { return <Todo key={`todo-${todo.id}`} todo={todo} />;  }) : "No todos, yay!"} </ul> ); 
Enter fullscreen mode Exit fullscreen mode

ここで <Todo/> の内部に渡せた
todo はデータだが、Todo は描画するためのコンポーネント。
かなりまぎわらしい。

AddTodo, Todo, VisibilityFilters,

これらも同じように connect を利用してデータとアクションをつなげる。

import React from "react"; import { connect } from "react-redux"; import cx from "classnames"; import { toggleTodo } from "../redux/actions"; const Todo = ({ todo, toggleTodo }) => ( <li className="todo-item" onClick={() => toggleTodo(todo.id)}> {todo && todo.completed ? "👌" : "👋"}{" "} <span className={cx( "todo-item__text", todo && todo.completed && "todo-item__text--completed" )} > {todo.content} </span>  </li> ); // export default Todo; export default connect( null, { toggleTodo } )(Todo); 
Enter fullscreen mode Exit fullscreen mode
import React from "react"; import { connect } from "react-redux"; import { addTodo } from "../redux/actions"; class AddTodo extends React.Component { constructor(props) { super(props); this.state = { input: "" }; } updateInput = input => { this.setState({ input }); }; handleAddTodo = () => { this.props.addTodo(this.state.input); this.setState({ input: "" }); }; render() { return ( <div> <input onChange={e => this.updateInput(e.target.value)} value={this.state.input} />  <button className="add-todo" onClick={this.handleAddTodo}> Add Todo </button>  </div>  ); } } export default connect( null, { addTodo } )(AddTodo); 
Enter fullscreen mode Exit fullscreen mode
import React from "react"; import cx from "classnames"; import { connect } from "react-redux"; import { setFilter } from "../redux/actions"; import { VISIBILITY_FILTERS } from "../constants"; const VisibilityFilters = ({ activeFilter, setFilter }) => { return ( <div className="visibility-filters"> {Object.keys(VISIBILITY_FILTERS).map(filterKey => { const currentFilter = VISIBILITY_FILTERS[filterKey]; return ( <span key={`visibility-filter-${currentFilter}`} className={cx( "filter", currentFilter === activeFilter && "filter--active" )} onClick={() => { setFilter(currentFilter); }} > {currentFilter} </span>  ); })} </div>  ); }; const mapStateToProps = state => { return { activeFilter: state.visibilityFilter }; }; // export default VisibilityFilters; export default connect( mapStateToProps, { setFilter } )(VisibilityFilters); 
Enter fullscreen mode Exit fullscreen mode

まとめ

React Redux アプリで Redux のロジックを分け、connect で繋ぐためには

redux/actionTypes.js で ADD_TODO, TOGGLE_TODO, SET_FILTER, の同名変数と文字列のマップを作って

redux/actions.js で type を actionTypes から当てて、 引数を paylaod にオブジェクトに入れる

redux/reducers/todo.js
redux/reducers/visibilityFilters.js

で initialState を作り、それを最初の state として、switch で
action.type ごとに渡された action.payload の値から state を更新する case を作る

これらを redux/reducers/index.js で combineReducers で一つにしてモジュールとして出力する

その reducers を redux/store.js で import して createStore して、またモジュールとして出力する

その store を src/index.js で Provider に結びつけて
内部に TodoApp という描画コンポーネントを描画する

その Components/TodoApp では AddTodo, TodoList, VisibilityFilters という描画コンポーネントをさらに描画し

Components/TodoList では todo の値を mapToStateProps と connectを使って getTodosByVisibilityFilter から取ってきている

getTodosByVisibilityFilter は redux/selectors.js にあって
visibilityFilter に応じて store から todo を取ってきている。

そして Components/TodoList で todo を Components/Todo に展開して渡す

Components/Todo では todo.completed がある時は 横線を引いたり、絵文字を変える className をつける。

こういう流れになっている。

Top comments (0)