Масштабирование с помощью редюсера и контекста
Редюсеры позволяют объединить логику обновления состояния компонента. Контекст позволяет передавать информацию глубоко вложенным компонентам. Вы можете объединить редюсеры и контекст, чтобы управлять состоянием сложного экрана.
Вы узнаете
- Как объединить редюсер с контекстом
- Как избежать передачи состояния и dispatch через пропсы
- Как хранить контекст и логику состояния в отдельном файле
Объединение редюсера с контекстом
В этом примере из введения в редюсеры состоянием управляет редюсер. Функция редюсера содержит всю логику обновления состояния и объявлена внизу этого файла:
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 } ];
Редюсер помогает сделать обработчики событий короткими и лаконичными. Однако по мере роста приложения может возникнуть другая сложность. Сейчас состояние tasks и функция dispatch доступны только в компоненте верхнего уровня TaskApp. Чтобы другие компоненты могли читать список задач или изменять его, нужно явно передавать вниз текущее состояние и обработчики событий, которые его изменяют, в качестве пропсов.
Например, TaskApp передаёт список задач и обработчики событий компоненту TaskList:
<TaskList
tasks={tasks}
onChangeTask={handleChangeTask}
onDeleteTask={handleDeleteTask}
/> А TaskList передаёт обработчики событий компоненту Task:
<Task
task={task}
onChange={onChangeTask}
onDelete={onDeleteTask}
/> В небольшом примере, таком как этот, такой подход работает хорошо, но если между компонентами находятся десятки или сотни других компонентов, передавать через них всё состояние и функции может быть очень утомительно!
Поэтому, вместо того чтобы передавать их через пропсы, можно поместить в контекст и состояние tasks, и функцию dispatch. Это позволит любому компоненту в дереве ниже TaskApp читать список задач и отправлять действия без повторяющегося «протаскивания пропсов».
Вот как можно объединить редюсер с контекстом:
- Создайте контекст.
- Поместите состояние и dispatch в контекст.
- Используйте контекст в любом месте дерева.
Шаг 1. Создайте контекст
Хук useReducer возвращает текущее tasks и функцию dispatch, которая позволяет обновлять их:
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks); Чтобы передать их вниз по дереву, создайте два отдельных контекста:
-
TasksContextпредоставляет текущий список задач. -
TasksDispatchContextпредоставляет функцию, с помощью которой компоненты могут отправлять действия.
Экспортируйте их из отдельного файла, чтобы позже импортировать в другие файлы:
import { createContext } from 'react'; export const TasksContext = createContext(null); export const TasksDispatchContext = createContext(null);
Здесь вы передаёте null в качестве значения по умолчанию для обоих контекстов. Фактические значения предоставит компонент TaskApp.
Шаг 2. Поместите состояние и dispatch в контекст
Теперь можно импортировать оба контекста в компонент TaskApp. Возьмите tasks и dispatch, возвращаемые функцией useReducer(), и предоставьте их всему дереву ниже:
import { TasksContext, TasksDispatchContext } from './TasksContext.js';
export default function TaskApp() {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
// ...
return (
<TasksContext value={tasks}>
<TasksDispatchContext value={dispatch}>
...
</TasksDispatchContext>
</TasksContext>
);
} Пока что вы передаёте информацию и через пропсы, и через контекст:
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 value={tasks}> <TasksDispatchContext value={dispatch}> <h1>Day off in Kyoto</h1> <AddTask onAddTask={handleAddTask} /> <TaskList tasks={tasks} onChangeTask={handleChangeTask} onDeleteTask={handleDeleteTask} /> </TasksDispatchContext> </TasksContext> ); } 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 value={tasks}>
<TasksDispatchContext value={dispatch}>
<h1>Day off in Kyoto</h1>
<AddTask />
<TaskList />
</TasksDispatchContext>
</TasksContext> Вместо этого любой компонент, которому нужен список задач, может получить его из TasksContext:
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 доступны каждому компоненту ниже по дереву: достаточно импортировать эти контексты и использовать их.
Перенос всей связующей логики в один файл
Это необязательно, но можно ещё больше упростить компоненты, переместив редюсер и контекст в один файл. Сейчас TasksContext.js содержит только два объявления контекстов:
import { createContext } from 'react';
export const TasksContext = createContext(null);
export const TasksDispatchContext = createContext(null); Этот файл скоро станет слишком большим! Вы переместите редюсер в тот же файл. Затем там же объявите новый компонент TasksProvider. Этот компонент объединит все части:
- Он будет управлять состоянием с помощью редюсера.
- Он предоставит оба контекста компонентам ниже.
- Он будет принимать
childrenв качестве пропса, чтобы вы могли передавать ему JSX.
export function TasksProvider({ children }) {
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
return (
<TasksContext value={tasks}>
<TasksDispatchContext value={dispatch}>
{children}
</TasksDispatchContext>
</TasksContext>
);
} Это избавит компонент 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 — способ обновлять их из любого компонента ниже по дереву.
Примечание
Функции, такие как useTasks и useTasksDispatch, называются пользовательскими хуками. Функция считается пользовательским хуком, если её имя начинается с use. Благодаря этому внутри неё можно использовать другие хуки, например useContext.
По мере роста приложения у вас может появиться много подобных пар «контекст — редюсер». Это эффективный способ масштабировать приложение и поднимать состояние выше, не прилагая больших усилий каждый раз, когда нужно получить доступ к данным глубоко в дереве.
Итоги
- Можно объединить редюсер с контекстом, чтобы любой компонент мог читать состояние и обновлять его.
- Чтобы предоставить состояние и функцию dispatch компонентам ниже:
- Создайте два контекста (для состояния и для функций dispatch).
- Предоставьте оба контекста из компонента, который использует редюсер.
- Используйте нужный контекст в компонентах, которым нужно его читать.
- Можно ещё больше упростить компоненты, переместив всю связующую логику в один файл.
- Можно экспортировать компонент, например
TasksProvider, который предоставляет контекст. - Также можно экспортировать пользовательские хуки, например
useTasksиuseTasksDispatch, чтобы читать его.
- Можно экспортировать компонент, например
- В приложении может быть много подобных пар «контекст — редюсер».
© 2013–present Facebook Inc.
Licensed under the Creative Commons Attribution 4.0 International Public License.
https://react.dev/learn/scaling-up-with-reducer-and-context