Reducerを使用すると、コンポーネントのstate更新ロジックを統合できます。Contextを使用すると、他のコンポーネントに情報を深く渡すことができます。ReducerとContextを組み合わせることで、複雑な画面のstateを管理できます。
このページで学ぶこと
- ReducerとContextを組み合わせる方法
- Propsを介したstateとdispatchの受け渡しを避ける方法
- Contextとstateロジックを別のファイルに保持する方法
ReducerとContextの組み合わせ
Reducerの入門のこの例では、stateはReducerによって管理されています。Reducer関数にはすべてのstate更新ロジックが含まれており、このファイルの最後に宣言されています。
import { useReducer } from 'react'; import AddTask from './AddTask.js'; import TaskList from './TaskList.js'; export default function TaskApp() { const [tasks, dispatch] = useReducer( tasksReducer, initialTasks ); function handleAddTask(text) { dispatch({ type: 'added', id: nextId++, text: text, }); } function handleChangeTask(task) { dispatch({ type: 'changed', task: task }); } function handleDeleteTask(taskId) { dispatch({ type: 'deleted', id: taskId }); } return ( <> <h1>Day off in Kyoto</h1> <AddTask onAddTask={handleAddTask} /> <TaskList tasks={tasks} onChangeTask={handleChangeTask} onDeleteTask={handleDeleteTask} /> </> ); } function tasksReducer(tasks, action) { switch (action.type) { case 'added': { return [...tasks, { id: action.id, text: action.text, done: false }]; } case 'changed': { return tasks.map(t => { if (t.id === action.task.id) { return action.task; } else { return t; } }); } case 'deleted': { return tasks.filter(t => t.id !== action.id); } default: { throw Error('Unknown action: ' + action.type); } } } let nextId = 3; const initialTasks = [ { id: 0, text: 'Philosopher’s Path', done: true }, { id: 1, text: 'Visit the temple', done: false }, { id: 2, text: 'Drink matcha', done: false } ];
Reducerはイベントハンドラを短く簡潔に保つのに役立ちます。ただし、アプリが成長するにつれて、別の困難に直面する可能性があります。現在、tasks
stateとdispatch
関数は、トップレベルのTaskApp
コンポーネントでのみ使用可能です。他のコンポーネントがタスクリストを読み取ったり変更したりできるようにするには、現在のstateと、それを変更するイベントハンドラをpropsとして明示的に渡す必要があります。
たとえば、TaskApp
はタスクリストとイベントハンドラをTaskList
に渡します。
<TaskList
tasks={tasks}
onChangeTask={handleChangeTask}
onDeleteTask={handleDeleteTask}
/>
そして、TaskList
はイベントハンドラをTask
に渡します。
<Task
task={task}
onChange={onChangeTask}
onDelete={onDeleteTask}
/>
このような小さな例では、これはうまく機能しますが、途中に数十または数百のコンポーネントがある場合、すべてのstateと関数を渡すのは非常にイライラする可能性があります!
そのため、propsを介してそれらを渡す代わりに、tasks
stateとdispatch
関数の両方をContextに入れることをお勧めします。これにより、ツリー内のTaskApp
より下のコンポーネントは、反復的な「prop drilling」なしに、タスクを読み取ってアクションをdispatchできます。
ReducerとContextを組み合わせる方法は次のとおりです。
- Contextを作成する。
- StateとdispatchをContextに入れる。
- ツリー内の任意の場所でContextを使用する。
ステップ1:Contextを作成する
useReducer
フックは、現在のtasks
と、それらを更新できるdispatch
関数を返します。
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
それらをツリーに渡すために、2つの別々のContextを作成します。
TasksContext
は、現在のタスクリストを提供します。TasksDispatchContext
は、コンポーネントがアクションをディスパッチできるようにする関数を提供します。
後で他のファイルからインポートできるように、別のファイルからエクスポートしてください
import { createContext } from 'react'; export const TasksContext = createContext(null); export const TasksDispatchContext = createContext(null);
ここでは、両方のコンテキストのデフォルト値としてnull
を渡しています。実際の値は、TaskApp
コンポーネントによって提供されます。
ステップ 2: state と dispatch をコンテキストに入れる
これで、両方のコンテキストをTaskApp
コンポーネントにインポートできます。useReducer()
によって返されるtasks
とdispatch
を受け取り、下のツリー全体に提供します
import { TasksContext, TasksDispatchContext } from './TasksContext.js';
export default function TaskApp() {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
// ...
return (
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
...
</TasksDispatchContext.Provider>
</TasksContext.Provider>
);
}
今のところ、プロパティとコンテキストの両方を介して情報を渡しています。
import { useReducer } from 'react'; import AddTask from './AddTask.js'; import TaskList from './TaskList.js'; import { TasksContext, TasksDispatchContext } from './TasksContext.js'; export default function TaskApp() { const [tasks, dispatch] = useReducer( tasksReducer, initialTasks ); function handleAddTask(text) { dispatch({ type: 'added', id: nextId++, text: text, }); } function handleChangeTask(task) { dispatch({ type: 'changed', task: task }); } function handleDeleteTask(taskId) { dispatch({ type: 'deleted', id: taskId }); } return ( <TasksContext.Provider value={tasks}> <TasksDispatchContext.Provider value={dispatch}> <h1>Day off in Kyoto</h1> <AddTask onAddTask={handleAddTask} /> <TaskList tasks={tasks} onChangeTask={handleChangeTask} onDeleteTask={handleDeleteTask} /> </TasksDispatchContext.Provider> </TasksContext.Provider> ); } function tasksReducer(tasks, action) { switch (action.type) { case 'added': { return [...tasks, { id: action.id, text: action.text, done: false }]; } case 'changed': { return tasks.map(t => { if (t.id === action.task.id) { return action.task; } else { return t; } }); } case 'deleted': { return tasks.filter(t => t.id !== action.id); } default: { throw Error('Unknown action: ' + action.type); } } } let nextId = 3; const initialTasks = [ { id: 0, text: 'Philosopher’s Path', done: true }, { id: 1, text: 'Visit the temple', done: false }, { id: 2, text: 'Drink matcha', done: false } ];
次のステップでは、プロパティの受け渡しを削除します。
ステップ 3: ツリー内の任意の場所でコンテキストを使用する
これで、タスクのリストやイベントハンドラーをツリーの下に渡す必要はありません
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
<h1>Day off in Kyoto</h1>
<AddTask />
<TaskList />
</TasksDispatchContext.Provider>
</TasksContext.Provider>
代わりに、タスクリストが必要なコンポーネントは、TaskContext
からそれを読み取ることができます
export default function TaskList() {
const tasks = useContext(TasksContext);
// ...
タスクリストを更新するために、任意のコンポーネントはコンテキストからdispatch
関数を読み取り、それを呼び出すことができます
export default function AddTask() {
const [text, setText] = useState('');
const dispatch = useContext(TasksDispatchContext);
// ...
return (
// ...
<button onClick={() => {
setText('');
dispatch({
type: 'added',
id: nextId++,
text: text,
});
}}>Add</button>
// ...
TaskApp
コンポーネントはイベントハンドラーを下に渡さず、TaskList
もTask
コンポーネントにイベントハンドラーを渡しません。 各コンポーネントは、必要なコンテキストを読み取ります。
import { useState, useContext } from 'react'; import { TasksContext, TasksDispatchContext } from './TasksContext.js'; export default function TaskList() { const tasks = useContext(TasksContext); return ( <ul> {tasks.map(task => ( <li key={task.id}> <Task task={task} /> </li> ))} </ul> ); } function Task({ task }) { const [isEditing, setIsEditing] = useState(false); const dispatch = useContext(TasksDispatchContext); let taskContent; if (isEditing) { taskContent = ( <> <input value={task.text} onChange={e => { dispatch({ type: 'changed', task: { ...task, text: e.target.value } }); }} /> <button onClick={() => setIsEditing(false)}> Save </button> </> ); } else { taskContent = ( <> {task.text} <button onClick={() => setIsEditing(true)}> Edit </button> </> ); } return ( <label> <input type="checkbox" checked={task.done} onChange={e => { dispatch({ type: 'changed', task: { ...task, done: e.target.checked } }); }} /> {taskContent} <button onClick={() => { dispatch({ type: 'deleted', id: task.id }); }}> Delete </button> </label> ); }
状態はまだ最上位のTaskApp
コンポーネントに「存在」し、useReducer
で管理されています。 しかし、そのtasks
とdispatch
は、これらのコンテキストをインポートして使用することにより、ツリー内の下のすべてのコンポーネントで利用できるようになりました。
すべての配線を1つのファイルに移動する
これを行う必要はありませんが、リデューサーとコンテキストの両方を1つのファイルに移動することで、コンポーネントをさらに整理できます。現在、TasksContext.js
には2つのコンテキスト宣言のみが含まれています
import { createContext } from 'react';
export const TasksContext = createContext(null);
export const TasksDispatchContext = createContext(null);
このファイルは、まもなく混雑します!リデューサーを同じファイルに移動します。次に、同じファイルに新しいTasksProvider
コンポーネントを宣言します。このコンポーネントは、すべてのピースを結び付けます。
- リデューサーで状態を管理します。
- 下のコンポーネントに両方のコンテキストを提供します。
- JSXを渡せるように、プロパティとして
children
を受け取ります。
export function TasksProvider({ children }) {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
return (
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
{children}
</TasksDispatchContext.Provider>
</TasksContext.Provider>
);
}
これにより、TaskApp
コンポーネントからすべての複雑さと配線が削除されます
import AddTask from './AddTask.js'; import TaskList from './TaskList.js'; import { TasksProvider } from './TasksContext.js'; export default function TaskApp() { return ( <TasksProvider> <h1>Day off in Kyoto</h1> <AddTask /> <TaskList /> </TasksProvider> ); }
TasksContext.js
からコンテキストを使用する関数をエクスポートすることもできます
export function useTasks() {
return useContext(TasksContext);
}
export function useTasksDispatch() {
return useContext(TasksDispatchContext);
}
コンポーネントがコンテキストを読み取る必要がある場合、これらの関数を介して行うことができます
const tasks = useTasks();
const dispatch = useTasksDispatch();
これにより動作はまったく変わりませんが、これらのコンテキストをさらに分割したり、これらの関数にロジックを追加したりできます。 これで、コンテキストとリデューサーの配線はすべてTasksContext.js
にあります。これにより、コンポーネントはクリーンで整理され、データの取得元ではなく、表示内容に集中できます。
import { useState } from 'react'; import { useTasks, useTasksDispatch } from './TasksContext.js'; export default function TaskList() { const tasks = useTasks(); return ( <ul> {tasks.map(task => ( <li key={task.id}> <Task task={task} /> </li> ))} </ul> ); } function Task({ task }) { const [isEditing, setIsEditing] = useState(false); const dispatch = useTasksDispatch(); let taskContent; if (isEditing) { taskContent = ( <> <input value={task.text} onChange={e => { dispatch({ type: 'changed', task: { ...task, text: e.target.value } }); }} /> <button onClick={() => setIsEditing(false)}> Save </button> </> ); } else { taskContent = ( <> {task.text} <button onClick={() => setIsEditing(true)}> Edit </button> </> ); } return ( <label> <input type="checkbox" checked={task.done} onChange={e => { dispatch({ type: 'changed', task: { ...task, done: e.target.checked } }); }} /> {taskContent} <button onClick={() => { dispatch({ type: 'deleted', id: task.id }); }}> Delete </button> </label> ); }
TasksProvider
をタスクの処理方法を知っている画面の一部、useTasks
をそれらを読み取る方法、useTasksDispatch
をツリー内の下の任意のコンポーネントからそれらを更新する方法と考えることができます。
アプリが成長するにつれて、このようなコンテキストとリデューサーのペアが多数存在する可能性があります。これは、アプリをスケーリングし、ツリーの深い場所にあるデータにアクセスしたいときに、あまり手間をかけずに状態を上位に移動するための強力な方法です。
要約
- リデューサーとコンテキストを組み合わせて、任意のコンポーネントが上位の状態を読み取り、更新できるようにします。
- 下のコンポーネントに状態とディスパッチ関数を提供するには
- 2つのコンテキストを作成します(状態用とディスパッチ関数用)。
- リデューサーを使用するコンポーネントから両方のコンテキストを提供します。
- それらを読み取る必要があるコンポーネントからいずれかのコンテキストを使用します。
- すべての配線を1つのファイルに移動することで、コンポーネントをさらに整理できます。
- コンテキストを提供する
TasksProvider
のようなコンポーネントをエクスポートできます。 - また、
useTasks
やuseTasksDispatch
のようなカスタムフックをエクスポートして、それを読み取ることもできます。
- コンテキストを提供する
- このように、アプリ内に多くのコンテキストとリデューサーのペアを持つことができます。