React Redux Tutorial Part 4 -- connect API の mapStateToProps を使って Todo アプリを作る
Tags: redux
Published: 2022-05-03 12:44:10


# 要件

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 を作成する。

```ts
export const ADD_TODO = "ADD_TODO";
export const TOGGLE_TODO = "TOGGLE_TODO";
export const SET_FILTER = "SET_FILTER";
```

必要なアクションは
* やることの追加
* やることのトグル(完了未完了の切り替え)
* 表示するやることリストのフィルターのセット

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

これが actionTypes 。

---

# redux/actions

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

```ts
import { ADD_TODO, TOGGLE_TODO, SET_FILTER } from "./actionTypes";
```

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

---

## addTodo

```ts
let nextTodoId = 0;

export const addTodo = content => ({
  type: ADD_TODO,
  payload: {
    id: ++nextTodoId,
    content
  }
});
```

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

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

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

---

## toggleTodo

```ts
export const toggleTodo = id => ({
  type: TOGGLE_TODO,
  payload: { id }
});
```

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

---

## setFilter

```ts
export const setFilter = filter => ({ type: SET_FILTER, payload: { filter } });
```

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 だけ

と解釈する。

```ts
import { ADD_TODO, TOGGLE_TODO } from "../actionTypes";
```

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

```ts
const initialState = {
  allIds: [],
  byIds: {}
};
```

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

```ts
export default function(state = initialState, action) {
  switch (action.type) {
    case ADD_TODO: {
//...
    }
    case TOGGLE_TODO: {
//...
    }
    default:
// ...
  }
}
```

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

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

---

## case ADD_TODO

ADD_TODO の時は

```ts
    case ADD_TODO: {
      const { id, content } = action.payload;
      return {
        ...state,
        allIds: [...state.allIds, id],
        byIds: {
          ...state.byIds,
          [id]: {
            content,
            completed: false
          }
        }
      };
    }
```

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

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

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

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

---

## case TOGGLE_TODO

```ts
    case TOGGLE_TODO: {
      const { id } = action.payload;
      return {
        ...state,
        byIds: {
          ...state.byIds,
          [id]: {
            ...state.byIds[id],
            completed: !state.byIds[id].completed
          }
        }
      };
    }
```

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

```ts
export const VISIBILITY_FILTERS = {
  ALL: "all",
  COMPLETED: "completed",
  INCOMPLETE: "incomplete"
};
```

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

---

## reducers/visibilityFilters.js

```ts
import { SET_FILTER } from "../actionTypes";
import { VISIBILITY_FILTERS } from "../../constants";
```

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

```ts
const initialState = VISIBILITY_FILTERS.ALL;
```

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

```ts
const visibilityFilter = (state = initialState, action) => {
  switch (action.type) {
    case SET_FILTER: {
      return action.payload.filter;
    }
    default: {
      return state;
    }
  }
};
```

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

```ts
export default visibilityFilter;
```

そして export する。自明。

これで reducers は全て作れた。

---

## reducers/index

```ts
import { combineReducers } from "redux";
import visibilityFilter from "./visibilityFilter";
import todos from "./todos";

export default combineReducers({ todos, visibilityFilter });
```

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

---
---

# redux/selectors.js

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

```ts
import { VISIBILITY_FILTERS } from "../constants";
```

フィルター定数を import して

```ts
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;
  }
};
```

しかしこれが
getTodos を使い

```ts
export const getTodos = store =>
  getTodoList(store).map(id => getTodoById(store, id));
```

getTodos が getTodoList と getTodoById を使い

```ts
export const getTodoList = store =>
  getTodosState(store) ? getTodosState(store).allIds : [];

export const getTodoById = (store, id) =>
  getTodosState(store) ? { ...getTodosState(store).byIds[id], id } : {};
```

getTodoList と getTodoById が getTodoState を使った。

```ts
export const getTodosState = store => store.todos;
```

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

---
---

# components/TodoList

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

```ts
import { getTodosByVisibilityFilter } from "../redux/selectors";

const mapStateToProps = state => {
  const { visibilityFilter } = state;
  const todos = getTodosByVisibilityFilter(state, visibilityFilter);
  return { todos };
}
```

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

```ts
import { connect } from "react-redux";
export default connect(mapStateToProps)(TodoList);
```

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

```ts
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>
);
```

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

## AddTodo, Todo, VisibilityFilters, 

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

```ts
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);
```

```ts
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);
```

```ts
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);

```

---

## まとめ

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

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