「React Hooks APIを活用することでReduxが不要になる」という話を聞いたことがある方もいるかもしれません。
今回は、Reduxの機能をReactのみで実装する検証として、React ReduxのBasic Tutorialで紹介されているサンプルアプリケーション(Todoアプリ)をReact Hooks APIで書き換えたので紹介します。
reactは17.0.1
、typescriptは3.8.3
を利用しています。
目次
- 1 今回作成するTodoアプリの概要
- 2 モックを作成する
- 3 React Contextを利用してグローバルなデータを扱えるようにする
- 4 React ContextとuseReducer()を組み合わせ、StateとDispatch関数をグローバルで扱えるようにする
- 5 useContext()を利用してコンポーネントからActionを実行(dispatch)できるようにする
- 6 useContext()を利用してコンポーネントでグローバルなStateを利用できるようにする
- 7 Todoのcompleted/incompleteが切替られるようにする
- 8 ステータスに応じてTodoをフィルタリングできるようにする
- 9 フィルタリング結果を画面に反映させる
- 10 まとめ
今回作成するTodoアプリの概要
- 「Add Todo」ボタンでTodoを追加できる
- Todoをクリックするとステータスのトグル操作(completed/incomplete)ができる
- all/completed/incompleteのステータスでTodoをフィルタリングできる
モックを作成する
create-react-app
を利用してアプリケーションのひな型を作成します。
### create-react-appを利用してアプリケーションを作成
$ npx create-react-app redux-todo-ts --template typescript
$ cd redux-todo-ts
### アプリケーションで必要になるnpmのインストール
$ yarn add classnames @types/classnames
### srcディレクトリのファイルを削除
$ cd redux-todo-app
$ rm -rf src/*
今回はCodeSandboxで公開されているTodoアプリのモックを利用しました。
当該モックはReduxを利用する前提であるため、React用にカスタマイズしました。
- TypeScript化
- reduxディレクトリの廃止
- Store(createStore)の削除
- Reducerの結合(combineReducers)の削除
- Selectorsの引数をStoreのStateから、コンテキストのStateに修正
- Reducerで定義していたinitialStateの削除
モックのディレクトリ構成は以下の通りです。
.
├── README.md
├── package.json
├── src
│ ├── TodoApp.tsx
│ ├── actionTypes.ts
│ ├── actions.ts
│ ├── components
│ │ ├── AddTodo.tsx
│ │ ├── Todo.tsx
│ │ ├── TodoList.tsx
│ │ └── VisibilityFilters.tsx
│ ├── constants.ts
│ ├── index.tsx
│ ├── react-app-env.d.ts
│ ├── reducers
│ │ ├── todos.ts
│ │ └── visibilityFilter.ts
│ ├── selectors.ts
│ ├── styles.css
│ └── types.ts
├── tsconfig.json
└── yarn.lock
モックのソースコードはnishina555/react-redux-todoapp-reacthooks-mockで公開しています。実際に手を動かして確認する際に活用してください。
React Contextを利用してグローバルなデータを扱えるようにする
React ContextはReduxのようにグローバルなデータの定義と参照ができるReactのAPIです。
React Contextには「Reduxなしでグローバルなデータの定義ができる」「Props drillingと呼ばれる、いわゆるpropsのバケツリレー問題を解決できる」などのメリットがあります。
React Contextを利用する場合、以下のような実装をします。
- React.createContextでコンテキスト(グローバルなデータ)を定義する
- Context.Providerでコンテキストを渡す
- Context.Consumerでコンテキストを受け取る
今回はReduxで実装していたグローバルなデータ管理をReact Contextで行います。
createContext()
でコンテキストを作成し、作成されたコンテキストを管理するProviderコンポーネントを用意します。
src/components/TodoProvider.tsx
import React from "react";
export const TodoContext = React.createContext("");
const TodoProvider: React.FC = ({ children }) => (
<TodoContext.Provider value={""}>{children}</TodoContext.Provider>
);
export default TodoProvider;
他のコンポーネントでコンテキストを利用できるようにするため、Providerコンポーネントで全体を囲います。
src/TodoApp.tsx
import React from "react";
import AddTodo from "./components/AddTodo";
import TodoList from "./components/TodoList";
import VisibilityFilters from "./components/VisibilityFilters";
import "./styles.css";
+ import TodoProvider from "./components/TodoProvider";
export default function TodoApp() {
return (
<div className="todo-app">
<h1>Todo List</h1>
+ <TodoProvider>
<AddTodo />
<TodoList />
<VisibilityFilters />
+ </TodoProvider>
</div>
);
}
React ContextとuseReducer()を組み合わせ、StateとDispatch関数をグローバルで扱えるようにする
関数コンポーネントでStateを管理する場合useState()
を利用します。useReducer()
は、より複雑なStateを管理したい場合に利用するuseState()
の代替となるHooksです。
useReducer()
はReducer関数((state, action) => newState
という型をもつ関数)を受け取り、StateとDispatch関数のペアを返します。
Stateの初期値はuseReducer()
の第2引数で指定します。(遅延初期化の場合は第2引数に初期値、第3引数に初期化関数を指定)
特に、React ContextとuseReducer()を組み合わせることで、StateとDispatch関数がグローバルで扱えます。
その結果、Reduxのように「各コンポーネントでActionを実行し、グローバルなStateを取得する」というアプリケーションが実現できます。
たとえば、各コンポーネントで「Actionの実行」「グローバルなStateの取得」をするには以下のようにします。
src/components/TodoProvider.tsx
import React, { useReducer } from "react";
import { TodoState } from "../types";
import { TodoActions } from "../actions";
import todoReducer from "../reducers/todos";
type TodoContextType = {
todos: TodoState;
dispatch: React.Dispatch<TodoActions>;
};
const initialState: TodoState = {
allIds: [],
byIds: {},
};
export const TodoContext = React.createContext({} as TodoContextType);
const TodoProvider: React.FC = ({ children }) => {
const [todos, dispatch] = useReducer(todoReducer, initialState);
return (
<TodoContext.Provider value={{ todos, dispatch }}>
{children}
</TodoContext.Provider>
);
};
export default TodoProvider;
useContext()を利用してコンポーネントからActionを実行(dispatch)できるようにする
useContext()
はReact Contextのしくみを簡単に利用できるようにしたHooksです。
Contex.Provider
によって渡されたコンテキストを受け取るContext.Consumer
は、useContext()
で代替できます。
たとえば、「Todoを追加する」というActionCreator(addTodo
)を利用する場合は以下のようになります。
useContext(TodoContext)
によってコンテキストで定義されているDispatch関数を受け取り、dispatch(addTodo)
を実行しています。
src/components/AddTodo.tsx
import React, { useState, useContext } from "react";
import { TodoContext } from "./TodoProvider";
import { addTodo } from "../actions";
const AddTodo: React.FC = () => {
const [input, setInput] = useState("");
const { dispatch } = useContext(TodoContext);
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;
以下のようにしてTodoContext
のtodos
をログに表示した場合、Add Todo
ボタンを押したタイミングでtodos
が変化していればOKです。
const AddTodo: React.FC = () => {
const [input, setInput] = useState("");
const { dispatch } = useContext(TodoContext);
// ActionのdispatchでStateが更新されるか確認
+ const todoState = useContext(TodoContext);
+ console.log(todoState.todos);
(略)
useContext()を利用してコンポーネントでグローバルなStateを利用できるようにする
コンテキストで管理されているStateをuseContext()
を利用して受け取り、コンポーネントに反映します。
const { todos } = useContext(TodoContext);
でtodos
というグローバルなStateがコンポーネントで扱えるようになります。
src/components/TodoList.tsx
import React, { useContext } from "react";
import Todo from "./Todo";
import { TodoContext } from "./TodoProvider";
import { getTodos } from "../selectors";
const TodoList = () => {
const { todos } = useContext(TodoContext);
const visibleTodos = getTodos(todos);
return (
<ul className="todo-list">
{visibleTodos && visibleTodos.length
? visibleTodos.map((todo, index) => {
return <Todo key={`todo-${todo.id}`} todo={todo} />;
})
: "No todos, yay!"}
</ul>
);
};
export default TodoList;
追加したTodoが画面に表示されればOKです。
Todoのcompleted/incompleteが切替られるようにする
前項と同様の手順で今度はTodoのステータスを切り替えるtoggleTodo
をコンポーネントで利用できるようにします。
const { dispatch } = useContext(TodoContext);
でDispatch関数を呼び出し、dispatch(toggleTodo(todo.id))
でActionを実行しています。
src/components/Todo.tsx
import React, { useContext } from "react";
import cx from "classnames";
import { TodoItem } from "../selectors";
import { TodoContext } from "./TodoProvider";
import { toggleTodo } from "../actions";
type TodoProps = {
todo: TodoItem;
};
const Todo: React.FC<TodoProps> = ({ todo }) => {
const { dispatch } = useContext(TodoContext);
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;
Todoをクリックし、ステータスが変更できればOKです。
ステータスに応じてTodoをフィルタリングできるようにする
Todoリストのフィルタ(VisibilityFilter
)を管理するReact Contextを作成します。
src/components/VisibilityFilterProvider.tsx
import React, { useReducer } from "react";
import { VISIBILITY_FILTERS } from "../constants";
import visibilityFilterReducer from "../reducers/visibilityFilter";
import { VisibilityFilterTypes } from "../types";
import { TodoActions } from "../actions";
type VisibilityFilterContextType = {
visibilityFilter: VisibilityFilterTypes;
dispatch: React.Dispatch<TodoActions>;
};
const initialState: VisibilityFilterTypes = VISIBILITY_FILTERS.ALL;
export const VisibilityFilterContext = React.createContext(
{} as VisibilityFilterContextType
);
const VisibilityFilterProvider: React.FC = ({ children }) => {
const [visibilityFilter, dispatch] = useReducer(
visibilityFilterReducer,
initialState
);
return (
<VisibilityFilterContext.Provider value={{ visibilityFilter, dispatch }}>
{children}
</VisibilityFilterContext.Provider>
);
};
export default VisibilityFilterProvider;
他のコンポーネントでコンテキストを利用できるようにするため、Providerコンポーネントで囲います。
src/TodoApp.tsx
import React from "react";
import AddTodo from "./components/AddTodo";
import TodoList from "./components/TodoList";
import VisibilityFilters from "./components/VisibilityFilters";
import "./styles.css";
import TodoProvider from "./components/TodoProvider";
+ import VisibilityFilterProvider from "./components/VisibilityFilterProvider";
export default function TodoApp() {
return (
<div className="todo-app">
<h1>Todo List</h1>
+ <VisibilityFilterProvider>
<TodoProvider>
<AddTodo />
<TodoList />
<VisibilityFilters />
</TodoProvider>
+ </VisibilityFilterProvider>
</div>
);
}
次にTodoリストのフィルタ情報を更新するコンポーネントにuseContext()
を追加し、コンテキストを扱えるようにします。
src/components/VisibilityFilters.tsx
import { useContext } from "react";
import cx from "classnames";
import { VISIBILITY_FILTERS } from "../constants";
import { setFilter } from "../actions";
import { VisibilityFilterContext } from "./VisibilityFilterProvider";
const VisibilityFilters = () => {
const { visibilityFilter, dispatch } = useContext(VisibilityFilterContext);
const activeFilter = 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;
フィルタリング結果を画面に反映させる
以下のようなSelectorメソッドを追加します。
src/selectors.ts
import { TodoState, VisibilityFilterTypes } from "./types";
import { VISIBILITY_FILTERS } from "./constants";
export const getTodosByVisibilityFilter = (
todos: TodoState,
visibilityFilter: VisibilityFilterTypes
): Array<TodoItem> => {
const allTodos = getTodos(todos);
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;
}
};
TodoList.tsx
を以下のように修正します。
src/components/TodoList.tsx
- import { getTodos } from "../selectors";
+ import { getTodosByVisibilityFilter } from "../selectors";
+ import { VisibilityFilterContext } from "./VisibilityFilterProvider";
const TodoList = () => {
const { todos } = useContext(TodoContext);
- const visibleTodos = getTodos(todos);
+ const { visibilityFilter } = useContext(VisibilityFilterContext);
+ const visibleTodos = getTodosByVisibilityFilter(todos, visibilityFilter);
return (
<ul className="todo-list">
{visibleTodos && visibleTodos.length
(略)
フィルタのリンク(all/completed/incomplete)をクリックし、画面のTodoがフィルタリングされていればOKです。
まとめ
- React Contextを利用することでグローバルなデータ(コンテキスト)が利用できる
- useReducer()はuseState()の代替となるReact Hooks。複雑なState管理をするときに利用する
- React ContextとuseReducer()を組み合わせることで、StateとDispatch関数がグローバルで扱える
- useContext()を利用することでコンテキストを取得できる
Twitter(@nishina555)やってます。フォローしてもらえるとうれしいです!