Масштабування з використанням контексту та редюсера
Редюсери дають змогу об’єднати логіку оновлення стану компонента. Контекст дає змогу передавати інформацію у глибину до інших компонентів. Можна поєднати редюсери і контекст, щоб управляти станом складного інтерфейсу.
You will learn
- Як поєднати з контекстом
- Як уникнути передачі стану та диспатча через пропси
- Як зберігати логіку контексту і стану в окремому файлі
Поєднання редюсера з контекстом
Як ми вже бачили, у прикладі з цього розділу, стан керується редюсером. Функція редюсера містить всю логіку оновлення стану і оголошена в кінці файлу:
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} dispatch={dispatch} />
```js
<TaskList
tasks={tasks} onChangeTask={handleChangeTask} onDeleteTask={handleDeleteTask}
/>
А TaskList
передає обробники компоненту Task
:
<Task
task={task}
onChange={onChangeTask}
onDelete={onDeleteTask}
/>
У невеликому прикладі це добре працює, але якщо у вас є десятки або сотні вкладених компонентів, передавати таким чином весь стан і функції може бути досить неприємно!
Тому, як альтернатива передачі через пропси, ви можете помістити і стан tasks
, і функцію dispatch
в контекст. Таким чином, будь-який компонент, що знаходиться нижче TaskApp
у дереві, може мати доступ та надсилати події, уникнувши передачі пропсів через безліч компонентів.
Ось як можна поєднати редюсер із контекстом:
- Створіть контекст.
- Розмістіть стан і диспетчер у контексті.
- Використовуйте контекст будь-де в дереві компонентів.
Крок 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.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 } ];
На наступному етапі видалимо передачу пропсів.
Step 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
- Ви можете об’єднати редюсер з контекстом, щоб будь-який компонент міг мати доступ та оновлювати стан у компонентах вищих за ієрархією.
- Щоб надати стан і функцію
dispatch
компонентам нижче:- Створіть два контексти (для стану і для функцій
dispatch
). - Надайте обидва контексти компоненту, який використовує редюсер.
- Використовуйте будь-який контекст у компонентах, яким потрібно мати ці дані.
- Створіть два контексти (для стану і для функцій
- Щоб ще більше спростити компоненти, виносьте ці налаштування в один файл.
- Експортуйте компонент, наприклад
TasksProvider
, який надає контекст. - Ви також можете експортувати користувацькі хуки, як-от
useTasks
таuseTasksDispatch
, для доступу до контексту.
- Експортуйте компонент, наприклад
- У додатку може бути багато схожих на цю пар контекст-редюсерів.