وقتی کامپوننتهای شما بزرگتر و پیچیدهتر میشوند، مدیریت حجم زیادی از آپدیتهای استیت که در رویدادهای مختلف (Event Handlers) پخش شدهاند، دشوار و کلافهکننده میشود. در این حالت، ریآکت راهکار فوقالعادهای به نام ردیوسر (Reducer) را در اختیار شما میگذارد تا تمام کدهای مربوط به تغییر وضعیت را در یک تابع واحد خارج از کامپوننت متمرکز کنید.
تابع ردیوسر چیست و چه کاربردی دارد؟
چطور کدهای خود را از useState به useReducer مهاجرت دهیم؟
چه زمانی باید از ردیوسر استفاده کرد؟
چطور یک ردیوسر استاندارد و تمیز بنویسیم؟
تصور کنید برنامهای برای مدیریت وظایف (Todo List) دارید. کامپوننت اصلی شما باید بتواند یک تسک را اضافه، ویرایش و یا حذف کند. در حالت عادی با useState، رویدادهای شما به این صورت پخش شدهاند:
// لایه رویدادها که مستقیماً در حال تغییر استیت هستند
function handleAddTask(text) {
setTasks([...tasks, { id: nextId++, text: text, done: false }]);
}
function handleChangeTask(task) {
setTasks(tasks.map(t => t.id === task.id ? task : t));
}
function handleDeleteTask(taskId) {
setTasks(tasks.filter(t => t.id !== taskId));
}
هرچه این کامپوننت بزرگتر شود، منطق تغییر استیت بیشتر در گوشه و کنار آن پخش میشود. برای حل این پیچیدگی، ردیوسرها به کمک ما میآیند. فرآیند مهاجرت از useState به useReducer شامل ۳ گام ساده است:
در معماری ردیوسر، شما به جای اینکه به ریآکت بگویید «چه کار کن» (تنظیم مستقیم استیت)، در رویدادها فقط اعلام میکنید که «کاربر چه کاری انجام داد». این یعنی به جای استفاده از متدِ setTasks، یک اکشن را به ردیوسر ارسال یا اصطلاحاً دیسپچ (Dispatch) میکنید.
یک اکشن، صرفاً یک شیء (Object) ساده جاوااسکریپتی است:
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,
});
}
📌 قرارداد: ساختار شیء اکشن دست شماست، اما طبق یک قرارداد جهانی، بهتر است یک کلید به نام
typeاز جنس رشته (String) داشته باشد که هدف و نیت کاربر را توصیف کند (مانند'added'یا'task_deleted').
تابع ردیوسر جایی است که منطق اصلی آپدیت استیت در آن قرار میگیرد. این تابع دو آرگومان میپذیرد: استیت فعلی (Current State) و شیء اکشن (Action). در نهایت، استیت بعدی (Next State) را محاسبه کرده و برمیگرداند:
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) => (t.id === action.task.id ? action.task : t));
}
case 'deleted': {
return tasks.filter((t) => t.id !== action.id);
}
default: {
throw Error('اکشن ناشناخته: ' + action.type);
}
}
}
💡 نکته: استفاده از شرطهای
switchبه جایif/elseدر ردیوسرها یک استاندارد رایج است زیرا خوانایی بالاتری دارد. همچنین توصیه میشود کدهای هرcaseرا داخل یک بلوک{ }قرار دهید تا متغیرهای تعریف شده در کیسهای مختلف با هم تداخل پیدا نکنند.
در نهایت هوک useReducer را از ریآکت ایمپورت کرده و جایگزین useState میکنید:
import { useReducer } from 'react';
import tasksReducer from './tasksReducer.js'; // میتوان ردیوسر را در فایلی کاملاً مجزا نوشت
const initialTasks = [
{ id: 0, text: 'بازدید از موزه', done: true },
{ id: 1, text: 'تماشای تئاتر', done: false }
];
export default function TaskApp() {
// useReducer تابع ردیوسر و استیت اولیه را میگیرد
// و استیت زنده به همراه تابع دیسپچ را خروجی میدهد
const [tasks, dispatch] = useReducer(tasksReducer, initialTasks);
// ... بقیه رویدادها دیسپچ را صدا میزنند
}
useState در برابر useReducerاین دو هوک از نظر فنی معادل هم هستند و هر کاری را با یکی انجام دهید با دیگری نیز شدنی است، اما تفاوتهای ساختاری مهمی دارند:
| معیار مقایسه | هوک useState | هوک useReducer |
| حجم کدنویسی اولیه | بسیار کم و سریع | بیشتر (نیاز به نوشتن اکشنها و تابع ردیوسر) |
| خوانایی در پروژههای بزرگ | ضعیف (منطق آپدیت لابلای کامپوننت گم میشود) | عالی (تفکیک کامل «چه اتفاقی افتاد» از «چگونه آپدیت شود») |
| سهولت در دیباگ (Debugging) | دشوار (مشخص نیست استیت کجا و چرا اشتباه ست شده) | بسیار عالی (با یک لگ گرفتن در ردیوسر تمام چرخه مشخص است) |
| تستنویسی (Testing) | وابسته به رندر کامپوننت | بسیار راحت (ردیوسر یک تابع خالص است و مستقل تست میشود) |
۱. ردیوسرها باید کاملاً خالص (Pure Functions) باشند: ردیوسرها مستقیماً در زمان رندر کامپوننت اجرا میشوند. این یعنی یک ردیوسر با ورودیهای یکسان، همیشه و همیشه باید خروجی یکسانی برگرداند. هرگز در ردیوسر درخواستهای API (مثل fetch)، تایمرها (setTimeout) یا کارهای جانبی (Side Effects) انجام ندهید. دیتای اشیاء و آرایهها را هم بدون دستکاری مستقیم (Mutation) و با کپی کردن آپدیت کنید.
۲. هر اکشن باید بیانگر یک تعامل واحد از سمت کاربر باشد: حتی اگر آن تعامل باعث تغییر چندین دیتا شود. به عنوان مثال، اگر کاربر دکمه «ریست فرم» را میزند که ۵ فیلد دارد، دیسپچ کردن یک اکشن با نام type: 'reset_form' بسیار منطقیتر و خواناتر از دیسپچ کردن ۵ اکشن مجزای set_field است.
درست مانند استیتهای معمولی، اگر تمایلی به استفاده فراوان از سینتکس کپی ... در آرایهها و اشیاء تودرتو درون ردیوسر ندارید، میتوانید از کتابخانه Immer و هوک useImmerReducer استفاده کنید. ایمر به شما اجازه میدهد استیت را به ظاهر به صورت مستقیم تغییر (Mutate) دهید، اما در پشت صحنه خودش کپی امن را برای ریآکت تولید میکند:
// نمونه کدهای خلاصه شده با Immer
case 'added': {
// به جای ریترن شیء جدید، مستقیماً به درافت پش میکنیم
draft.push({
id: action.id,
text: action.text,
done: false
});
break;
}
ردیوسرها کدهای مربوط به تغییر استیت را از داخل رویدادهای کامپوننت به یک تابع متمرکز بیرونی هدایت میکنند.
تبدیل به ردیوسر شامل ۳ گام است: دیسپچ اکشنها، نوشتن تابع ردیوسر، اعمال هوک useReducer.
ردیوسرها باید توابع خالص (pure) باشند و هیچ ساید افکتی ایجاد نکنند.
هر اکشن باید یک تراکنش و رفتار معنایی کامل از سمت کاربر را توصیف کند.
این محتوا کاملا رایگان توسط تیم کدلپر ترجمه شده و در اختیار شما کاربران عزیز قرار گرفته است، هر گونه کپی برداری برای مقاصد غیر رایگان و بدون ذکر منبع، مورد پیگیری قانونی قرار میگیرد.
ترجمه شده از منبع: https://react.dev/learn