Movatterモバイル変換


[0]ホーム

URL:


Is this page useful?

Scaling Up with Reducer and Context

Reducers let you consolidate a component’s state update logic. Context lets you pass information deep down to other components. You can combine reducers and context together to manage state of a complex screen.

You will learn

  • How to combine a reducer with context
  • How to avoid passing state and dispatch through props
  • How to keep context and state logic in a separate file

Combining a reducer with context

In this example fromthe introduction to reducers, the state is managed by a reducer. The reducer function contains all of the state update logic and is declared at the bottom of this file:

Fork
import{useReducer}from'react';importAddTaskfrom'./AddTask.js';importTaskListfrom'./TaskList.js';exportdefaultfunctionTaskApp(){const[tasks,dispatch] =useReducer(tasksReducer,initialTasks);functionhandleAddTask(text){dispatch({type:'added',id:nextId++,text:text,});}functionhandleChangeTask(task){dispatch({type:'changed',task:task});}functionhandleDeleteTask(taskId){dispatch({type:'deleted',id:taskId});}return(<><h1>Day off in Kyoto</h1><AddTaskonAddTask={handleAddTask}/><TaskListtasks={tasks}onChangeTask={handleChangeTask}onDeleteTask={handleDeleteTask}/></>);}functiontasksReducer(tasks,action){switch(action.type){case'added':{return[...tasks,{id:action.id,text:action.text,done:false}];}case'changed':{returntasks.map(t=>{if(t.id ===action.task.id){returnaction.task;}else{returnt;}});}case'deleted':{returntasks.filter(t=>t.id !==action.id);}default:{throwError('Unknown action: ' +action.type);}}}letnextId =3;constinitialTasks =[{id:0,text:'Philosopher’s Path',done:true},{id:1,text:'Visit the temple',done:false},{id:2,text:'Drink matcha',done:false}];

A reducer helps keep the event handlers short and concise. However, as your app grows, you might run into another difficulty.Currently, thetasks state and thedispatch function are only available in the top-levelTaskApp component. To let other components read the list of tasks or change it, you have to explicitlypass down the current state and the event handlers that change it as props.

For example,TaskApp passes a list of tasks and the event handlers toTaskList:

<TaskList
tasks={tasks}
onChangeTask={handleChangeTask}
onDeleteTask={handleDeleteTask}
/>

AndTaskList passes the event handlers toTask:

<Task
task={task}
onChange={onChangeTask}
onDelete={onDeleteTask}
/>

In a small example like this, this works well, but if you have tens or hundreds of components in the middle, passing down all state and functions can be quite frustrating!

This is why, as an alternative to passing them through props, you might want to put both thetasks state and thedispatch functioninto context.This way, any component belowTaskApp in the tree can read the tasks and dispatch actions without the repetitive “prop drilling”.

Here is how you can combine a reducer with context:

  1. Create the context.
  2. Put state and dispatch into context.
  3. Use context anywhere in the tree.

Step 1: Create the context

TheuseReducer Hook returns the currenttasks and thedispatch function that lets you update them:

const[tasks,dispatch] =useReducer(tasksReducer,initialTasks);

To pass them down the tree, you willcreate two separate contexts:

  • TasksContext provides the current list of tasks.
  • TasksDispatchContext provides the function that lets components dispatch actions.

Export them from a separate file so that you can later import them from other files:

Fork
import{createContext}from'react';exportconstTasksContext =createContext(null);exportconstTasksDispatchContext =createContext(null);

Here, you’re passingnull as the default value to both contexts. The actual values will be provided by theTaskApp component.

Step 2: Put state and dispatch into context

Now you can import both contexts in yourTaskApp component. Take thetasks anddispatch returned byuseReducer() andprovide them to the entire tree below:

import{TasksContext,TasksDispatchContext}from'./TasksContext.js';

exportdefaultfunctionTaskApp(){
const[tasks,dispatch] =useReducer(tasksReducer,initialTasks);
// ...
return(
<TasksContextvalue={tasks}>
<TasksDispatchContextvalue={dispatch}>
...
</TasksDispatchContext>
</TasksContext>
);
}

For now, you pass the information both via props and in context:

Fork
import{useReducer}from'react';importAddTaskfrom'./AddTask.js';importTaskListfrom'./TaskList.js';import{TasksContext,TasksDispatchContext}from'./TasksContext.js';exportdefaultfunctionTaskApp(){const[tasks,dispatch] =useReducer(tasksReducer,initialTasks);functionhandleAddTask(text){dispatch({type:'added',id:nextId++,text:text,});}functionhandleChangeTask(task){dispatch({type:'changed',task:task});}functionhandleDeleteTask(taskId){dispatch({type:'deleted',id:taskId});}return(<TasksContextvalue={tasks}><TasksDispatchContextvalue={dispatch}><h1>Day off in Kyoto</h1><AddTaskonAddTask={handleAddTask}/><TaskListtasks={tasks}onChangeTask={handleChangeTask}onDeleteTask={handleDeleteTask}/></TasksDispatchContext></TasksContext>);}functiontasksReducer(tasks,action){switch(action.type){case'added':{return[...tasks,{id:action.id,text:action.text,done:false}];}case'changed':{returntasks.map(t=>{if(t.id ===action.task.id){returnaction.task;}else{returnt;}});}case'deleted':{returntasks.filter(t=>t.id !==action.id);}default:{throwError('Unknown action: ' +action.type);}}}letnextId =3;constinitialTasks =[{id:0,text:'Philosopher’s Path',done:true},{id:1,text:'Visit the temple',done:false},{id:2,text:'Drink matcha',done:false}];

In the next step, you will remove prop passing.

Step 3: Use context anywhere in the tree

Now you don’t need to pass the list of tasks or the event handlers down the tree:

<TasksContextvalue={tasks}>
<TasksDispatchContextvalue={dispatch}>
<h1>Day off in Kyoto</h1>
<AddTask/>
<TaskList/>
</TasksDispatchContext>
</TasksContext>

Instead, any component that needs the task list can read it from theTasksContext:

exportdefaultfunctionTaskList(){
consttasks =useContext(TasksContext);
// ...

To update the task list, any component can read thedispatch function from context and call it:

exportdefaultfunctionAddTask(){
const[text,setText] =useState('');
constdispatch =useContext(TasksDispatchContext);
// ...
return(
// ...
<buttononClick={()=>{
setText('');
dispatch({
type:'added',
id:nextId++,
text:text,
});
}}>Add</button>
// ...

TheTaskApp component does not pass any event handlers down, and theTaskList does not pass any event handlers to theTask component either. Each component reads the context that it needs:

Fork
import{useState,useContext}from'react';import{TasksContext,TasksDispatchContext}from'./TasksContext.js';exportdefaultfunctionTaskList(){consttasks =useContext(TasksContext);return(<ul>{tasks.map(task=>(<likey={task.id}><Tasktask={task}/></li>))}</ul>);}functionTask({task}){const[isEditing,setIsEditing] =useState(false);constdispatch =useContext(TasksDispatchContext);lettaskContent;if(isEditing){taskContent =(<><inputvalue={task.text}onChange={e=>{dispatch({type:'changed',task:{...task,text:e.target.value}});}}/><buttononClick={()=>setIsEditing(false)}>          Save</button></>);}else{taskContent =(<>{task.text}<buttononClick={()=>setIsEditing(true)}>          Edit</button></>);}return(<label><inputtype="checkbox"checked={task.done}onChange={e=>{dispatch({type:'changed',task:{...task,done:e.target.checked}});}}/>{taskContent}<buttononClick={()=>{dispatch({type:'deleted',id:task.id});}}>        Delete</button></label>);}

The state still “lives” in the top-levelTaskApp component, managed withuseReducer. But itstasks anddispatch are now available to every component below in the tree by importing and using these contexts.

Moving all wiring into a single file

You don’t have to do this, but you could further declutter the components by moving both reducer and context into a single file. Currently,TasksContext.js contains only two context declarations:

import{createContext}from'react';

exportconstTasksContext =createContext(null);
exportconstTasksDispatchContext =createContext(null);

This file is about to get crowded! You’ll move the reducer into that same file. Then you’ll declare a newTasksProvider component in the same file. This component will tie all the pieces together:

  1. It will manage the state with a reducer.
  2. It will provide both contexts to components below.
  3. It willtakechildren as a prop so you can pass JSX to it.
exportfunctionTasksProvider({children}){
const[tasks,dispatch] =useReducer(tasksReducer,initialTasks);

return(
<TasksContextvalue={tasks}>
<TasksDispatchContextvalue={dispatch}>
{children}
</TasksDispatchContext>
</TasksContext>
);
}

This removes all the complexity and wiring from yourTaskApp component:

Fork
importAddTaskfrom'./AddTask.js';importTaskListfrom'./TaskList.js';import{TasksProvider}from'./TasksContext.js';exportdefaultfunctionTaskApp(){return(<TasksProvider><h1>Day off in Kyoto</h1><AddTask/><TaskList/></TasksProvider>);}

You can also export functions thatuse the context fromTasksContext.js:

exportfunctionuseTasks(){
returnuseContext(TasksContext);
}

exportfunctionuseTasksDispatch(){
returnuseContext(TasksDispatchContext);
}

When a component needs to read context, it can do it through these functions:

consttasks =useTasks();
constdispatch =useTasksDispatch();

This doesn’t change the behavior in any way, but it lets you later split these contexts further or add some logic to these functions.Now all of the context and reducer wiring is inTasksContext.js. This keeps the components clean and uncluttered, focused on what they display rather than where they get the data:

Fork
import{useState}from'react';import{useTasks,useTasksDispatch}from'./TasksContext.js';exportdefaultfunctionTaskList(){consttasks =useTasks();return(<ul>{tasks.map(task=>(<likey={task.id}><Tasktask={task}/></li>))}</ul>);}functionTask({task}){const[isEditing,setIsEditing] =useState(false);constdispatch =useTasksDispatch();lettaskContent;if(isEditing){taskContent =(<><inputvalue={task.text}onChange={e=>{dispatch({type:'changed',task:{...task,text:e.target.value}});}}/><buttononClick={()=>setIsEditing(false)}>          Save</button></>);}else{taskContent =(<>{task.text}<buttononClick={()=>setIsEditing(true)}>          Edit</button></>);}return(<label><inputtype="checkbox"checked={task.done}onChange={e=>{dispatch({type:'changed',task:{...task,done:e.target.checked}});}}/>{taskContent}<buttononClick={()=>{dispatch({type:'deleted',id:task.id});}}>        Delete</button></label>);}

You can think ofTasksProvider as a part of the screen that knows how to deal with tasks,useTasks as a way to read them, anduseTasksDispatch as a way to update them from any component below in the tree.

Note

Functions likeuseTasks anduseTasksDispatch are calledCustom Hooks. Your function is considered a custom Hook if its name starts withuse. This lets you use other Hooks, likeuseContext, inside it.

As your app grows, you may have many context-reducer pairs like this. This is a powerful way to scale your app andlift state up without too much work whenever you want to access the data deep in the tree.

Recap

  • You can combine reducer with context to let any component read and update state above it.
  • To provide state and the dispatch function to components below:
    1. Create two contexts (for state and for dispatch functions).
    2. Provide both contexts from the component that uses the reducer.
    3. Use either context from components that need to read them.
  • You can further declutter the components by moving all wiring into one file.
    • You can export a component likeTasksProvider that provides context.
    • You can also export custom Hooks likeuseTasks anduseTasksDispatch to read it.
  • You can have many context-reducer pairs like this in your app.


[8]ページ先頭

©2009-2025 Movatter.jp