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

kaede_io

kaede

Posted on May 3, 2022

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 をつける。

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

💖 💪 🙅 🚩
kaede_io
kaede

Posted on May 3, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related