درک مفهوم Reducer در Redux

Redux یکی از محبوب‌ترین انتخاب‌ها برای مدیریت state در برنامه‌های فرانت‌اند است. اما همیشه یک شکایت در مورد آن وجود داشته است، این که «درک Redux سخت است!». بسیاری از توسعه‌دهندگان، Redux را گیج‌کننده می‌دانند. اما این مشکل به این دلیل است که Redux از مفاهیم برنامه نویسی functional پیروی می‌کند که بسیاری از توسعه‌دهندگان به آن عادت ندارند. همین موضوع می‌تواند چالشی برای یادگیری Redux به حساب بیاید. در این مقاله قصد داریم تا بیشتر به مفاهیم اولیه و پایه‌ای بپردازیم و مفهوم Reducer در Redux را بررسی کنیم.

Reducer چیست؟

Reducer چیست؟ و چگونه می‌توانیم بدون اینکه کم و کاستی در کد پیش بیاید یک Reducer بنویسیم؟

در اینجا از یک تشبیه استفاده می‌کنیم که می‌تواند درک مفهوم Reducer در Redux را آسان‌تر کند و آن «قهوه ساز» است. قهوه ساز پودر قهوه و آب را می‌گیرد سپس یک فنجان قهوه تازه دم شده را برمی‌گرداند که می‌توانیم از آن لذت ببریم. بر اساس این مقایسه Reducerها توابعی هستند که در state فعلی (پودر قهوه) و actionها (آب)، state جدید (قهوه تازه) می‌سازند.

Reducerها توابع pure هستند که براساس state و action فعلی state جدیدی را برمی‌گردانند.

یک Reducer همیشه باید قوانین زیر را رعایت کند:

با توجه به مجموعه‌ای از ورودی‌ها، همیشه باید همان خروجی ثابت را برگرداند، بدون هیچ side effect، فراخوانی API و تغییری.

منظور از توابع pure چیست؟

Reducerهای Redux توابع pure هستند. اما منظور از توابع pure چیست؟

اگر تابعی از قوانین زیر پیروی کند pure در نظر گرفته می‌شود:

  1. یک تابع اگر آرگومان‌های یکنواخت دریافت کند، همیشه همان خروجی ثابت را برمی‌گرداند.
  2. تابع هیچ‌گونه side effectای ایجاد نمی‌کند.

همه ما بدون اینکه اطلاع داشته باشیم توابع pure را نوشته‌ایم و از آن‌ها استفاده کرده‌ایم.

به عنوان مثال به تابع pure زیر دقت کنید که مجموع دو عدد را با توجه به دو آرگومان ورودی برمی‌گرداند.

function sumOfNumbers(a, b) {
 return a + b;
}

خروجی این تابع در صورتی که همان آرگومان‌های ورودی وارد شوند، ثابت می‌ماند. توابع pure بلاک‌های کد ساده‌ای هستند که همیشه رفتار قابل پیش‌بینی دارند. از این رو آن‌ها همیشه همان خروجی قابل پیش‌بینی را بدون هیچ‌گونه تأثیرپذیری خارجی برمی‌گردانند.

همچنین در ادامه یک مثال ساده از یک تابع impure و نحوه عملکرد آن بررسی کرده‌ایم:

var value = 5;
function impure(arg) {
    return value + 2 + 3;
}

در مثال بالا تابع impure در نظر گرفته می‌شود، زیرا از آرگومانی که به آن ارسال شده است استفاده نمی‌کند. در عوض، از یک مقدار خارجی استفاده می‌کند که در معرض تغییر است. بنابراین، به دلیل تداخل و یا side effectهای خارجی تابع impure می‌شود.

منظور از side effect چیست؟

زمانی که تابع مورد نظر ما نیاز به تعامل با دنیای بیرون داشه باشد side effect رخ می‎‌دهد.

چند نمونه از side effectهای رایج عبارتند از:

  • فراخوانی یک API
  • تغییر داده‌ها
  • استفاده از console.log
  • دستکاری DOM
  • استفاده از ()Date.now برای دریافت تاریخ/زمان فعلی
  • فراخوانی و یا انتظار async await برای resolve کردن promiseها
  • استفاده از ()math.random برای تولید اعداد تصادفی

ممکن است تعجب کنید که چگونه می‌توانیم توابع را بدون side effectها بنویسیم. اجتناب از side effectها به طور کلی کار بسیار چالش برانگیزی است. اما باید به این نکته توجه کنیم برای اینکه یک تابع، همانطور که از نامش پیداست، pure باشد، نباید هیچ گونه side effectای داشته باشد.

عدم تغییر state در داخل Reducer

اکنون که با مفهوم تابع pure آشنا شدیم آماده هستیم تا Reducer های خود را در redux بنویسیم. باید این موضوع را به خاطر داشته باشیم که هرگز نباید state را در داخل Reducer تغییر دهیم و این کار را می توانیم با استفاده از ()Object.assign و یا آخرین عملگر (…)Object Spread انجام دهیم.

باید به این نکته توجه کنیم که هرگز نباید state را تغییر دهیم، بلکه همیشه باید یک کپی جدید از state را برگردانیم.

از آنجایی که عملگر spread ساده‌تر است و آخرین نسخه‌های جاوااسکریپت هم از آن پشتیبانی می‌کنند، قصد داریم از آن در مثال خود استفاده کنیم. همچنین عملگر object spread از نظر مفهومی شبیه به عملگر array spread در ES6 است.

در دامه مثالی را بررسی می‌کنیم که در آن قرار است به پست وبلاگ امتیاز دهیم. Reducer ما state و action قدیمی را انجام خواهد داد. action در اینجا RATE_BLOG_POST است.

متد Reducer به نام ()blogDetails استیت و action فعلی را به عنوان پارامتر در نظر گرفته و state جدیدی را برمی‌گرداند.

// Reducer
function blogDetails(state = initialState, action) {
  switch (action.type) {
     case RATE_BLOG_POST:
       return { 
         ...state, rating: action.rate 
       }
     default:
       return state
    }
 }

در مثال بالا، قصد داریم تا همیشه یک کپی جدید از state را برگردانیم. عملگر spread (…) اطمینان حاصل می‌کند که در تابع Reducer هیچ تغییری در state اتفاق نیفتاده است.

Reducerهای متعدد برای برنامه‌های بزرگ‌تر

Redux برای حفظ یک حافظه تغییرناپذیر از اصل حافظه منفرد(single store) پیروی می‌کند. اما این امکان را به ما می‌دهد تا چند Reducer داشته باشیم. همانطور که برنامه ما شروع به رشد می‌کند، می‌توانیم Reducerهای متعددی داشته باشید که با functionalityها از هم جدا شده‌اند. این امر تفکیک موضوعات را در بین Reducerهای متعدد آسان‌تر می‌کند. در مثال زیر دو Reducer به نام‌های A و B داریم. و تابع combinationReducer از redux می‌تواند برای ترکیب Reducerها و ارسال آن‌ها به حافظه واحد استفاده شود.

import { createStore, combineReducers } from 'redux';  
// The A Reducer 
const AReducer = function(state = {}, action) {   
   return state 
}  
// The B Reducer 
const BReducer = function(state = {}, action) {   
   return state 
}  
// Combine Reducers 
const reducers = combineReducers({   
   AState: AReducer,   
   BState: BReducer 
})  
const store = createStore(reducers)

مزایای Reducerها

Reducer‌‌‌ ها بخش اصلی ادغام redux هستند و بسیاری از منطق بیزینس را حفظ می‌کنند. از آنجایی که Reducerها برای توابع pure طراحی شده‌اند مزایای زیادی را به‌ همراه دارند. از جمله:

  • منطق بیزینس در توابع تمیز، ساده و ظریف حفظ می‌شود زیرا آن‌ها توابع pure هستند.
  • Reducerها توابع pure هستند و بنابراین نگهداری، تست و دیباگ کردن و اشکال‌زدایی آن‌ها آسان‌تر است.
  • همچنین گسترش و افزودن functionality اضافی به Reducerها آسان است.

البته نوشتن توابع pure تمیز حتی اگر از redux استفاده نکنیم امکان‌پذیر است. اما وقتی از redux در برنامه‌های پیچیده برای مدیریت state استفاده می‌کنیم، رعایت این اصول بسیار عالی است.

دیدگاه‌ها:

افزودن دیدگاه جدید