要件
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 をつける。
こういう流れになっている。
Top comments (0)