要件
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)