توسعه با Reducer و Context
reducer به شما این امکان رو میدهد که بتوانید state کامپوننت مورد نظرتان را یکپارچه کنید. Context این امکان رو به شما می دهد که اطلاعات را به کامپوننت های دیگر انتقال دهید. شما میتوانید با ادغام reducers و context با هم دیگر state محتواهای پیچیده را مدیریت کنید.
You will learn
- چگونه reducer و context را با یکدیگر ترکیب کنید
- چگونه از پاسکاری state و dispatch به وسیله props جلوگیری کنید
- چگونه از context و منطق state در فایل های جداگانه، نگهداری کنید
ترکیب یک reducer با context
در این مثال از معرفی reducers، استیت توسط یک 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 به کوتاه و مختصر بودن event handler ها کمک میکند. به هر حال زمانی که مقیاس برنامه شما بزرگتر می شود، ممکن است با مشکل دیگری رو به رو شوید. در حال حاضر، state tasks
و تابع dispatch
تنها در بالاترین لایهی کامپوننت TaskApp
در دسترس هستند. برای اینکه باقی کامپوننت ها نیز قابلیت دسترسی و تغییر لیست تسک ها را داشته باشند، باید state فعلی و event handler هایی که به عنوان props آن را تغییر میدهند، را منتقل کنید.
به عنوان مثال، TaskApp
یک لیست از tasks و event handlers را به TaskList
منتقل میکند:
<TaskList
tasks={tasks}
onChangeTask={handleChangeTask}
onDeleteTask={handleDeleteTask}
/>
و TaskList
event handler ها را به Task
ارسال می کند:
<Task
task={task}
onChange={onChangeTask}
onDelete={onDeleteTask}
/>
در مثال های کوچک مانند این مساله، این روش خوب کار میکند، اما اگر شما دهها یا صدها کامپوننت در میانه داشته باشید، انتقال تمام state و توابع میتواند کاری خستهکننده باشد!
در همین راستا، به عنوان یک جایگزین برای انتقال آنها از طریق props، ممکن است بخواهید هم state tasks
و هم تابع dispatch
را در context قرار دهید . به این ترتیب، هر کامپوننتی زیر TaskApp
در درخت میتواند وظایف را بخواند و اقدامات dispatch را بدون تکرار “prop drilling” انجام دهد.
در ادامه نحوه ترکیب یک reducer با context را مشاهده میکنید:
- ایجاد context.
- قراردادن state و dispatch در context.
- استفاده از context در هر قسمتی از این درخت.
گام 1: ایجاد context
هوک useReducer
مقدار tasks
را برمیگرداند و تابع dispatch
امکان به روزرسانی آنها را فراهم میکند:
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
برای انتقال آنها در درخت، شما میتوانید دو context جداگانه ایجاد کنید:
TasksContext
لیستی از task ها را فراهم میکند.TasksDispatchContext
تابعی را میسازد که کامپوننت ها بتوانند اکشن ها را dispatch کنند.
آنها را از یک فایل جداگانه export کنید تا بتوانید بعداً آنها را از فایلهای دیگر import کنید:
import { createContext } from 'react'; export const TasksContext = createContext(null); export const TasksDispatchContext = createContext(null);
در اینجا، شما null
را به عنوان مقدار پیشفرض به هر دو context ارسال میکنید. مقادیر واقعی توسط کامپوننت TaskApp
ارائه خواهد شد.
گام 2: قراردادن state و dispatch در context
الان میتوانید هر دوی context ها را در کامپوننت TaskApp
import کنید. 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>
);
}
در حال حاضر، شما اطلاعات را هم از طریق props و هم در context ارسال میکنید:
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 } ];
در گام بعدی، شما پاس دادن props را حذف خواهید کرد.
گام 3: استفاده از context در هر قسمتی از درخت
اکنون نیازی به انتقال لیست task ها یا event handler به پایین درخت ندارید:
<TasksContext.Provider value={tasks}>
<TasksDispatchContext.Provider value={dispatch}>
<h1>Day off in Kyoto</h1>
<AddTask />
<TaskList />
</TasksDispatchContext.Provider>
</TasksContext.Provider>
به جای آن، هر کامپوننتی که به لیست tasks نیاز دارد، میتواند آن را از TaskContext
بخواند:
export default function TaskList() {
const tasks = useContext(TasksContext);
// ...
برای بهروزرسانی لیست tasks، هر کامپوننت میتواند تابع dispatch
را از context بخواند و آن را فراخوانی کند:
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
دیگر هیچ event handler را به پایین منتقل نمیکند، و TaskList
نیز هیچ event handler را به کامپوننت Task
منتقل نمیکند. هر کامپوننت context مورد نیاز خود را میخواند:
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
آن اکنون توسط هر کامپوننت زیرین در درخت با وارد کردن و استفاده از این contextها در دسترس است.
جابجایی تمام اتصالات به یک فایل
شما نیازی به انجام این کار ندارید، اما میتوانید با جابجایی هم reducer و هم context را در یک فایل تمیزتر کنید. در حال حاضر، TasksContext.js
فقط شامل دو اعلان context است:
import { createContext } from 'react';
export const TasksContext = createContext(null);
export const TasksDispatchContext = createContext(null);
این فایل در حال شلوغ و پیچیده شدن میباشد! reducer را به فایل مشابه منتقل کنید، سپس یک کامپوننت TasksProvider
را در همان فایل ایجاد کنید. این کامپوننت تمام قطعات را به هم متصل می کند:
- state را همراه با یک reducer مدیریت میکند.
- هر دو context را برای کامپوننتهای زیرین فراهم میکند.
children
را به عنوان یک prop منتقل میکند تا بتوانید 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>Day off in Kyoto</h1> <AddTask /> <TaskList /> </TasksProvider> ); }
همچنین میتوانید توابعی را که از context استفاده میکنند از TasksContext.js
export کنید.
export function useTasks() {
return useContext(TasksContext);
}
export function useTasksDispatch() {
return useContext(TasksDispatchContext);
}
هنگامی که یک کامپوننت نیاز به خواندن context دارد، میتواند این کار را از طریق این توابع انجام دهد.
const tasks = useTasks();
const dispatch = useTasksDispatch();
این به هیچ وجه رفتار را تغییر نمیدهد، اما به شما امکان میدهد که در آینده این context را بیشتر تقسیم کنید یا برخی منطق به این توابع اضافه کنید. اکنون تمام اتصالات context و reducer در 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
به عنوان یک بخش از صفحه فکر کنید که نحوه برخورد با task ها را میشناسد، useTasks
را به عنوان یک روش برای خواندن آنها و useTasksDispatch
را به عنوان یک راه برای بهروزرسانی آنها از هر کامپوننتی در زیر درخت، در نظر بگیرید.
هر چقدر که برنامهی شما بزرگتر میشود، ممکن است چندین جفت context-reducer هایی را داشته باشید. این روشی قدرتمند برای افزایش مقیاس برنامه شماست و به شما امکان میدهد که state را در درخت پخش کنید بدون نیاز به انجام کار بیشتر، هر زمان که میخواهید به دادههای درون درخت دسترسی داشته باشید.
Recap
- شما میتوانید یک reducer را با context ترکیب کنید تا هر کامپوننتی بتواند state را خوانده و بهروزرسانی کند.
- برای انتشار state و تابع dispatch به کامپوننتهای پایینتر:
- دو context (برای state و برای توابع dispatch) ایجاد کنید.
- هر دو context را با توجه به کامپوننتی که از reducer استفاده میکند، ایجاد کنید.
- از context ای استفاده کنید که مرتبط با کامپوننتهایی که نیاز به خواندن دارند، باشد.
- میتوانید با جابجایی همه اتصالات در یک فایل، کامپوننتها را بهتر مدیریت کنید.
- میتوانید یک کامپوننت مانند
TasksProvider
را که context فراهم میکند، export کنید. - همچنین میتوانید Custom Hookهایی مانند
useTasks
وuseTasksDispatch
برای خواندن آن استفاده کنید.
- میتوانید یک کامپوننت مانند
- میتوانید در برنامهی خود چندین جفت context-reducer مانند این داشته باشید.