React Redux Tutorial Part 4 -- connect API の mapStateToProps を使って Todo アプリを作る
kaede
Posted on May 3, 2022
要件
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";
必要なアクションは
- やることの追加
- やることのトグル(完了未完了の切り替え)
- 表示するやることリストのフィルターのセット
これらの 3 つになる。
なのでこれら 3 つ文字列で定数として定義する。
これが actionTypes 。
redux/actions
次に今作った actionTypes を元にした actions を作成する
import { ADD_TODO, TOGGLE_TODO, SET_FILTER } from "./actionTypes";
actionTypes を import する
これらの文字列の定数たちをアクションごとの types という識別名に使う。
addTodo
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
export const toggleTodo = id => ({
type: TOGGLE_TODO,
payload: { id }
});
id を受け取り、payload に渡し、
type に TOGGLE_TODO をつける
setFilter
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 だけ
と解釈する。
import { ADD_TODO, TOGGLE_TODO } from "../actionTypes";
actionTypes から やることの追加とやることの切り替えの定数を import
const initialState = {
allIds: [],
byIds: {}
};
初期ステートの中に全ての ID の配列、個別の ID のオブジェクト
これらを空で定義する。
ここからグローバルステートが作られ、保持されていく。
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 の時は
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
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
export const VISIBILITY_FILTERS = {
ALL: "all",
COMPLETED: "completed",
INCOMPLETE: "incomplete"
};
表示フィルターの定数の定義。自明。
reducers/visibilityFilters.js
import { SET_FILTER } from "../actionTypes";
import { VISIBILITY_FILTERS } from "../../constants";
最初に作った actionTypes から SET_FILTER の定数を import
直前で作った constants から VISIBILITY_FILTERS の
定数オブジェクトを import
const initialState = VISIBILITY_FILTERS.ALL;
保持されるグローバルステートの初期値を all にする
const visibilityFilter = (state = initialState, action) => {
switch (action.type) {
case SET_FILTER: {
return action.payload.filter;
}
default: {
return state;
}
}
};
SET_FILTER が呼ばれた時に、action.payload の filter から
受け取った filter の値をそのまま返すシンプルな処理。
export default visibilityFilter;
そして export する。自明。
これで reducers は全て作れた。
reducers/index
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 しか使われていないように見える。
import { VISIBILITY_FILTERS } from "../constants";
フィルター定数を 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;
}
};
しかしこれが
getTodos を使い
export const getTodos = store =>
getTodoList(store).map(id => getTodoById(store, id));
getTodos が getTodoList と getTodoById を使い
export const getTodoList = store =>
getTodosState(store) ? getTodosState(store).allIds : [];
export const getTodoById = (store, id) =>
getTodosState(store) ? { ...getTodosState(store).byIds[id], id } : {};
getTodoList と getTodoById が getTodoState を使った。
export const getTodosState = store => store.todos;
結局、entire file を使うことになった。。。
components/TodoList
描画コンポーネントの TodoList で、selectors の
import { getTodosByVisibilityFilter } from "../redux/selectors";
const mapStateToProps = state => {
const { visibilityFilter } = state;
const todos = getTodosByVisibilityFilter(state, visibilityFilter);
return { todos };
}
getTodosByVisibilityFilter を使う。todos を持ってくるために使った。
import { connect } from "react-redux";
export default connect(mapStateToProps)(TodoList);
これが 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>
);
ここで <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);
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);
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 をつける。
こういう流れになっている。
Posted on May 3, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.