Масштабирование с помощью редюсера и контекста
Редюсеры позволяют объединить и упорядочить логику обновления состояния. Контекст позволяет передавать состояние глубоко в дочерние компоненты. Вы можете объединить редюсеры и контекст для управления комплексом состояний.
Вы узнаете
- Как объединить редюсер и контекст
- Как избежать передачи состояния и отправителя через пропсы
- Как хранить логику контекста и состояния в отдельном файле
Объединение редюсера с контекстом
В этом примере из введения в редюсеры, состояние контролируется редюсером. Функция редюсер содержит в себе всю логику обновления состояния и объявлена в нижней части этого файла:
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>День в Киото</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: 'Тропа философа', done: true }, { id: 1, text: 'Посетить храм', done: false }, { id: 2, text: 'Выпить маття', 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
получить доступ к tasks
и отправлять действия без передачи пропсов через компоненты которым они не нужны.
Вот как вы можешь объединить редюсер с контекстом:
- Создайте контекст.
- Поместите состояние и функцию отправитель в контекст.
- Используйте контекст в любых дочерних элементах.
Шаг 1: Создайте контекст
Хук useReducer
возвращает текущее состояние tasks
и функцию dispatch
которая позволяет его обновлять:
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
Что бы передать их дальше по дереву вам надо создать два раздельных контекста:
TasksContext
содержит текущий список задач.TasksDispatchContext
содержит функции которая позволяет компонентам отправлять действия(dispatch
).
Экспортируйте их из разных файлов что бы потом импортировать в других файлах:
import { createContext } from 'react'; export const TasksContext = createContext(null); export const TasksDispatchContext = createContext(null);
В этом примере вы можете передать null
как значение по умолчанию в каждый контекст. Актуальное значение передаст компонент TaskApp
.
Шаг 2: Поместите состояние и отправителя в контекст
Сейчас вы можете импортировать контексты которые создавали в компонент TaskApp
. Поместите tasks
и dispatch
возвращенные хуком useReducer()
и передайте их во все дочерние компоненты:
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>День в Киото</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: 'Тропа философа', done: true }, { id: 1, text: 'Посетить храм', done: false }, { id: 2, text: 'Выпить маття', done: false } ];
На следующем шаге, вы избавитесь от пропсов.
Шаг 3: Используйте контекст в любом месте дерева
Теперь вам не надо передавать список задач или слушатели событий в дочернии компоненты:
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
<h1>День в Киото</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,
});
}}>Добавить</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)}> Сохранить </button> </> ); } else { taskContent = ( <> {task.text} <button onClick={() => setIsEditing(true)}> Редактировать </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 }); }}> Удалить </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.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>День в Киото</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)}> Сохранить </button> </> ); } else { taskContent = ( <> {task.text} <button onClick={() => setIsEditing(true)}> Редактировать </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 }); }}> Удалить </button> </label> ); }
Можно представить TasksProvider как часть интерфейса, которая знает, как работать с задачами, useTasks — как способ их чтения, а useTasksDispatch — как способ обновления из любого дочернего компонента.
По мере роста вашего приложения у вас может появиться много таких контекст-редюсер пар. Это мощный способ масштабировать приложение и передавать состояние без лишних усилий, когда вам нужно получить доступ к данным глубоко в дереве.
Recap
- Вы можете комбинировать редюсер с контекстом, что бы любой компонент мог получить доступ к состоянию родительского компонента и изменять его
- Чтобы предоставить состояние и функцию отправителя в дочернии компоненты, надо:
- Создать два контекста (для состояния и функций отправителей).
- Экспортируйте оба контекста из компонента в котором объявлен редюсер.
- Используйте оба контекста в компонентах где вам это нужно.
- Можно ещё больше упростить компоненты, переместив всю логику создания в один файл.
- Вы можете экспортировать компонент, например,
TasksProvider
, который предоставляет контекст. - Также можно экспортировать пользовательские хуки, такие как
useTasks
иuseTasksDispatch
, для доступа к данным.
- Вы можете экспортировать компонент, например,
- В вашем приложении может быть много таких контекст-редюсер пар.