React Context APIをState、Dispatch専用に分けて使う方法
ReactのContext APIを使う時、コードの構造をどうするかについては決まっている方法はありません。Conext APIを作成するためにクラスコンポーネントをつかっても関数コンポーネントをつかっても問題はありません。関数コンポーネントを使う時もuseReducerもuseStateも使えます。
今回はContext APIを使ってTodoListを作りながら、Context APIを活用する色々な方法の中、個人的に好きな方法を紹介したいと思います。
プロジェクト準備
以下のCodeSandboxからContext API適用前の簡単なTodoListを確認してください。ここからContext APIを作成していきたいと思います。各コンポーネントの役割はコンポーネント名の通りです。
Context作成
それでは、Context APIを作成します。まずsrc/contexts/TodosContext.tsx
ファイルを作ります。これからTodosContext.tsx
に2つのContextを作ってみます。1つはState専用、もう1つはDispatch専用のContextです。このように2つのContextを作ると、無駄なレンダリングを防ぐことができます。
StateとDispatchを1つのContextにまとめて入れると、TodoFormのようなStateは要らなくDispatchだけ必要なコンポーネントもStateが更新される時に再レンダリングされます。
src/contexts/TodosContext.tsx
import { createContext, Dispatch, ReactNode } from 'react';
export type Todo = {
id: number;
text: string;
done: boolean;
};
type TodosState = Todo[];
// * State専用 Context *
// 今後 Providerを使わない時にはContextの値がundefinedになる必要があるので,
// Contextの値がTodosStateにもundefinedにもできるように宣言してください。
const TodosStateContext = createContext<TodosState | undefined>(undefined);
type Action =
| { type: 'CREATE'; text: string }
| { type: 'TOGGLE'; id: number }
| { type: 'REMOVE'; id: number };
type TodosDispatch = Dispatch<Action>;
// * Dispatch専用 Context *
const TodosDispatchContext = createContext<TodosDispatch | undefined>(
undefined
);
// * Reducer *
function todosReducer(state: TodosState, action: Action): TodosState {
switch (action.type) {
case 'CREATE':
const nextId = Math.max(...state.map(todo => todo.id)) + 1;
return state.concat({
id: nextId,
text: action.text,
done: false
});
case 'TOGGLE':
return state.map(todo =>
todo.id === action.id ? { ...todo, done: !todo.done } : todo
);
case 'REMOVE':
return state.filter(todo => todo.id !== action.id);
default:
throw new Error('Invalid action');
}
}
// TodosStateContextとTodosDispatchContextのProviderを一緒に使います。
export function TodosContextProvider({ children }: { children: ReactNode }) {
const [todos, dispatch] = useReducer(todosReducer, [
{
id: 1,
text: 'Context APIを勉強する',
done: true
},
{
id: 2,
text: 'TypeScriptを勉強する',
done: true
},
{
id: 3,
text: 'TypeScriptでContext APIを使ってみる',
done: false
}
]);
return (
<TodosDispatchContext.Provider value={dispatch}>
<TodosStateContext.Provider value={todos}>
{children}
</TodosStateContext.Provider>
</TodosDispatchContext.Provider>
);
}
カスタムHooks作成
TodosStateContextとTodosDispatchContextを使う時は以下のようにuseContextを使ってContextの値が使えるようになります。
const todos = useContext(TodosStateContext);
ここで、todosのtypeはTodoState | undefined
なので使う前に値をチェックする必要があります。
const todos = useContext(TodosStateContext);
if (!todos) return null;
このように値をチェックして使用しても問題はありませんが、より良い方法としてはTodosContext専用Hooksを作って使用する方法があります。
// src/contexts/TodosContext.tsx
import { createContext, Dispatch, useReducer, useContext } from 'react';
(...前のコード省略)
export function useTodosState() {
const state = useContext(TodosStateContext);
if (!state) throw new Error('TodosProvider not found');
return state;
}
export function useTodosDispatch() {
const dispatch = useContext(TodosDispatchContext);
if (!dispatch) throw new Error('TodosProvider not found');
return dispatch;
}
関数から必要な値がない時はエラーを出すので各Hookが返す値のtypeは有効である保障できます。
src/App.tsx
import TodoForm from './components/TodoForm';
import TodoList from './components/TodoList';
import { TodosContextProvider } from './contexts/TodosContext';
const App = () => {
return (
<TodosContextProvider>
<TodoForm />
<TodoList />
</TodosContextProvider>
);
};
export default App;
src/components/TodoList.tsx
import TodoItem from './TodoItem';
import { useTodosState } from '../contexts/TodosContext';
function TodoList() {
const todos = useTodosState();
return (
<ul>
{todos.map(todo => (
<TodoItem todo={todo} key={todo.id} />
))}
</ul>
);
}
export default TodoList;
src/components/TodoForm.tsx
import { useState, FormEvent } from 'react';
import { useTodosDispatch } from '../contexts/TodosContext';
function TodoForm() {
const [value, setValue] = useState('');
const dispatch = useTodosDispatch();
const onSubmit = (e: FormEvent) => {
e.preventDefault();
dispatch({
type: 'CREATE',
text: value
});
setValue('');
};
return (
<form onSubmit={onSubmit}>
<input
value={value}
placeholder="やることを入力してくださいー"
onChange={e => setValue(e.target.value)}
/>
<button>追加</button>
</form>
);
}
export default TodoForm;
src/components/TodoItem.tsx
一般的な場合、Contextを使わない環境ではTodoItemコンポーネントからonToggle
、onRemove
をpropsとして受けて、また呼び出す構造が多いと思います。
今回はContextを使っているのでpropsではなく直接コンポーネントからaction dispatchしてみます。
import './TodoItem.css';
import { useTodosDispatch, Todo } from '../contexts/TodosContext';
type TodoItemProps = {
todo: Todo;
};
function TodoItem({ todo }: TodoItemProps) {
const dispatch = useTodosDispatch();
const onToggle = () => {
dispatch({
type: 'TOGGLE',
id: todo.id
});
};
const onRemove = () => {
dispatch({
type: 'REMOVE',
id: todo.id
});
};
return (
<li className={`TodoItem ${todo.done ? 'done' : ''}`}>
<span className="text" onClick={onToggle}>
{todo.text}
</span>
<span className="remove" onClick={onRemove}>
(X)
</span>
</li>
);
}
export default TodoItem;
まとめ
Context APIを使ってStateを管理する時はuseReducerを使ってState専用とDispatch専用Contextを作って使うと便利です。Contextを作った後Contextをより便利に使うためのカスタムHooksまで作るとさらに楽な開発になれると思います。
Discussion