React Redux hooksはconnect()
の代わりとなるAPIです。
React Redux hooks APIを利用することでReduxのStateやActionの実行(dispatch)がconnect()
なしで行えます。
React Redux hooks APIには以下の3つがあります。
- useSelector()
- useDispatch()
- useStore()
今回はReact ReduxのBasic Tutorialで紹介されているサンプルアプリケーション(Todoアプリ)をReact Redux hooks APIで書き換えたので紹介します。
コードはTypeScriptで記述しています。React ReduxのチュートリアルをTypeScriptに書き換える手順は『React Reduxのチュートリアル(Todoアプリ)をTypeScriptで書き換えてみた』を参考にしてください。
react-reduxは7.1.16
、reactは17.0.1
、typescriptは3.8.3
を利用しています。
目次
下準備: AddTodoをクラスコンポーネントから関数コンポーネントにする
hooks APIを使う下準備としてAddTodo.tsx
をクラスコンポーネントから関数コンポーネントに書き換えます。
クラスコンポーネントから関数コンポーネントに書き換える際の変更点は以下の通りです。
- constructorを削除
- thisを削除
- AddTodoStateを削除し、代わりにuseState()を追加
- 型定義をReact.ComponentからReact.FCにする
src/components/AddTodo.tsx
import React, { useState } from "react";
import { connect } from "react-redux";
import { addTodo } from "../redux/actions";
import { TodoActions } from "../redux/actions";
type AddTodoProps = {
addTodo: (input: string) => TodoActions;
};
const AddTodo: React.FC<AddTodoProps> = ({ addTodo }) => {
const [input, setInput] = useState("");
const updateInput = (input: string) => {
setInput(input);
};
const handleAddTodo = () => {
addTodo(input);
setInput("");
};
return (
<div>
<input onChange={(e) => updateInput(e.target.value)} value={input} />
<button className="add-todo" onClick={handleAddTodo}>
Add Todo
</button>
</div>
);
};
export default connect(null, { addTodo })(AddTodo);
mapDispatchToPropsの代わりにuseDispatch()を利用する
useDispatch()
はconnect()
でいうところのmapDispatchToProps
にあたります。
まずはDispatchの型を定義します。
Redux Toolkitを利用している場合、Dispatchの型は以下のように取得します。1
src/redux/store.ts
import { createStore } from "redux";
import rootReducer from "./reducers";
const store = createStore(
rootReducer,
(window as any).__REDUX_DEVTOOLS_EXTENSION__ &&
(window as any).__REDUX_DEVTOOLS_EXTENSION__()
);
export default store;
export type AppDispatch = typeof store.dispatch
たとえばAddTodo.tsx
の場合、useDispatch()
を利用してaddTodo
を実行するには以下のようになります。
const dispatch: AppDispatch = useDispatch();
でdispatch
を利用できるようにし、dispatch(addTodo(input));
でActionをdispatchしています。
src/components/AddTodo.tsx
import React, { useState } from "react";
import { addTodo } from "../redux/actions";
import { useDispatch } from "react-redux";
import { AppDispatch } from "../redux/store";
const AddTodo: React.FC = () => {
const dispatch: AppDispatch = useDispatch();
const [input, setInput] = useState("");
const updateInput = (input: string) => {
setInput(input);
};
const handleAddTodo = () => {
dispatch(addTodo(input));
setInput("");
};
return (
<div>
<input onChange={(e) => updateInput(e.target.value)} value={input} />
<button className="add-todo" onClick={handleAddTodo}>
Add Todo
</button>
</div>
);
};
export default AddTodo;
Actionを実行しているそのほかのコンポーネントの修正は以下の通りです。
src/components/Todo.tsx
import React from "react";
import cx from "classnames";
import { toggleTodo } from "../redux/actions";
import { TodoItem } from "../redux/selectors";
import { useDispatch } from "react-redux";
import { AppDispatch } from "../redux/store";
type TodoProps = {
todo: TodoItem;
};
const Todo: React.FC<TodoProps> = ({ todo }) => {
const dispatch: AppDispatch = useDispatch();
return (
<li className="todo-item" onClick={() => dispatch(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;
src/components/VisibilityFilters.tsx
import React from "react";
import cx from "classnames";
import { VISIBILITY_FILTERS } from "../constants";
import { connect } from "react-redux";
import { setFilter } from "../redux/actions";
import { State, VisibilityFilterTypes } from "../redux/types";
import { AppDispatch } from "../redux/store";
import { useDispatch } from "react-redux";
type VisibilityFiltersProps = {
activeFilter: VisibilityFilterTypes;
};
const VisibilityFilters: React.FC<VisibilityFiltersProps> = ({
activeFilter,
}) => {
const dispatch: AppDispatch = useDispatch();
return (
<div className="visibility-filters">
{(Object.keys(VISIBILITY_FILTERS) as Array<
keyof typeof VISIBILITY_FILTERS
>).map((filterKey) => {
const currentFilter = VISIBILITY_FILTERS[filterKey];
return (
<span
key={`visibility-filter-${currentFilter}`}
className={cx(
"filter",
currentFilter === activeFilter && "filter--active"
)}
onClick={() => dispatch(setFilter(currentFilter))}
>
{currentFilter}
</span>
);
})}
</div>
);
};
const mapStateToProps = (state: State) => {
return { activeFilter: state.visibilityFilter };
};
export default connect(mapStateToProps)(VisibilityFilters);
mapStateToPropsの代わりにuseSelector()を利用する
useSelector()
はconnect()
でいうところのmapStateToProps
にあたります。
useSelector()
に書き換えると以下のようになります。
mapStateToProps
の代わりにuseSelector()
を利用してtodos
を取得するようにしています。
src/components/TodoList.tsx
import React from "react";
import Todo from "./Todo";
import { State } from "../redux/types";
import { getTodosByVisibilityFilter, TodoItem } from "../redux/selectors";
import { useSelector } from "react-redux";
const TodoList: React.FC = () => {
const todos: Array<TodoItem> = useSelector((state: State) => {
const { visibilityFilter } = state;
return getTodosByVisibilityFilter(state, visibilityFilter);
});
return (
<ul className="todo-list">
{todos && todos.length
? todos.map((todo: TodoItem, index: number) => {
return <Todo key={`todo-${todo.id}`} todo={todo} />;
})
: "No todos, yay!"}
</ul>
);
};
export default TodoList;
同様の方法でVisibilityFilters
も修正します。
src/components/VisibilityFilters.tsx
import React from "react";
import cx from "classnames";
import { VISIBILITY_FILTERS } from "../constants";
import { setFilter } from "../redux/actions";
import { State, VisibilityFilterTypes } from "../redux/types";
import { AppDispatch } from "../redux/store";
import { useDispatch, useSelector } from "react-redux";
const VisibilityFilters: React.FC = () => {
const dispatch: AppDispatch = useDispatch();
const activeFilter: VisibilityFilterTypes = useSelector(
(state: State) => state.visibilityFilter
);
return (
<div className="visibility-filters">
{(Object.keys(VISIBILITY_FILTERS) as Array<
keyof typeof VISIBILITY_FILTERS
>).map((filterKey) => {
const currentFilter = VISIBILITY_FILTERS[filterKey];
return (
<span
key={`visibility-filter-${currentFilter}`}
className={cx(
"filter",
currentFilter === activeFilter && "filter--active"
)}
onClick={() => dispatch(setFilter(currentFilter))}
>
{currentFilter}
</span>
);
})}
</div>
);
};
export default VisibilityFilters;
参考: useSelector()を利用する際の注意点
useSelector()
ではconnect()
と違い、キャッシュの利用可否の判定に===
(厳密な比較)が使われます。2
ですので、useSelector()
でオブジェクトを取得する場合はパフォーマンス面の注意が必要です。
なぜならオブジェクトは===
(厳密な比較)において、プロパティのスカラ値が等しい場合(= オブジェクトの値が同じ場合)も、参照先が異なればfalse
になるためです。
その結果、オブジェクトの見かけ上の値は変わっていないのにもかかわらずキャッシュが利用されないというケースが発生します。
useSelector()
でオブジェクトを取得する場合、キャッシュを活用するには浅い比較(shallow equal)を利用する必要があります。
浅い比較であれば、別インスタンスであってもプロパティのスカラ値が一致していればオブジェクトの比較はtrue
になります。(= キャッシュが有効になる。)
import { shallowEqual, useSelector } from 'react-redux'
// useSelector()で、'==='の代わりに浅い比較を利用する方法
const selectedData = useSelector(selectorReturningObject, shallowEqual)
===
と浅い比較の違いは以下の通りです。
import { shallowEqual } from 'react-redux'
const a = { value: "hoge" }
const b = { value: "hoge" }
// === の場合はfalseになる
// つまり、オブジェクトのプロパティの値が同じでもuseSelector()ではキャッシュを利用しない
console.log(a === b)
// => false
// 浅い比較において、a, b はtrueになる
console.log(shallowEqual(a, b))
// => true
補足: useStore()について
今回紹介していないuseStore()
について補足説明をします。
useStore()
は<Provider store={store}>
で渡しているStoreの情報(State)をコンポーネントで直接参照できるhooks APIです。
しかしuseStore()
でStateの情報を取得した場合、Stateの更新に対してコンポーネントが連動しません。そのため、Stateを利用する場合は原則useSelector()
を利用するのが好ましいです。
useStore()
は「Reducerを置き換える場合」など、Storeの情報を直接参照しないといけないようなケースでのみ利用します。3
以上の理由から今回の記事ではuseStore()
は紹介しませんでした。
最終的なアプリケーション
まとめ
- React Redux hooks APIにはuseSelector(), useDispatch(), useStore()がある
- React Redux hooks APIを利用することでconnect()が不要になる
- useSelector()はmapStateToPropsの代わりになるもの
- useSelector()でオブジェクトを取得する際はパフォーマンス面に注意する
- useDispatch()はmapDispatchToPropsの代わりになるもの
- Stateの情報を取得するときは原則useStore()ではなくuseSelector()を利用する
Twitter(@nishina555)やってます。フォローしてもらえるとうれしいです!