ساخت Reducer تغییرناپذیر در ریداکس — از صفر تا صد

۴۱ بازدید
آخرین به‌روزرسانی: ۰۳ مهر ۱۴۰۲
زمان مطالعه: ۳ دقیقه
ساخت Reducer تغییرناپذیر در ریداکس — از صفر تا صد

ریداکس (Redux) اساساً تنها یک سیستم اشتراک پیام‌رسانی یک‌طرفه محسوب می‌شود، اما با بهره گرفتن از آن می‌توانیم یک سیستم مدیریت حالت بسازیم که به طرز شگفت‌انگیزی مقیاس‌پذیر است. در این سیستم پیام‌رسانی می‌توانیم در رویدادها مشترک شویم، یعنی آن‌ها را مشاهده (Observe) کنیم یا به آن‌ها گوش (Listen) دهیم. این کار همانند فراخوانی یک تابع برای ایجاد یک درخواست به یک API است. نکته جالب‌تر این است که در این اشتراک می‌توان داده‌هایی نیز ارسال کرد. در این مقاله با روش ساخت Reducer تغییرناپذیر در ریداکس آشنا خواهیم شد.

در ریداکس در زمان اجرا (Runtime) اشتراک‌ها تنظیم و stroe-ها اعلان می‌شوند. گردش داده در ریداکس به این صورت است که ابتدا اکشن با یا بدون داده dispatch می‌شود. سپس مشترکان اطلاع‌رسانی می‌شوند و در نهایت داده‌ها در store سراسری تنظیم می‌شوند. همچنان که می‌بینید ریداکس کاملاً ماژولار و ساده است. فراموش نکنید که پیاده‌سازی شما از ریداکس باید پرتابل، تعویض‌پذیر و تا حدود زیادی قابل جداسازی از اپلیکیشن باشد. در ادامه این مقاله تلاش می‌کنیم یک پیاده‌سازی با این خصوصیات بسازیم.

در ریداکس با داده‌ها چه کنیم؟

اگر تاکنون به دنبال روش‌های مدیریت منطق در اپلیکیشن‌های ریداکس بوده باشید، احتمالاً با روش‌های بسیار گوناگونی مواجه شده‌اید. برخی می‌گویند که منطق باید در Reducer قرار گیرند، برخی محل آن را در اکشن می‌دانند و برخی دیگر نیز بر این باورند که نباید هیچ منطقی در ردیوسر یا اکشن قرار گیرد. ما نیز با این نظر اخیر موافق هستیم و معتقدیم نباید هیچ منطقی در ردیوسر یا اکشن‌ها وجود داشته باشد و باید کاملاً خالص و تابعی باشند.

پس منطق را کجا بگذاریم؟

برای این که راه‌حل‌های ما مقیاس‌پذیر و تمیز باشند دو گزینه وجود دارد:

  • میان‌افزار منطقی برای ریداکس طراحی کنیم.
  • منطق را خارج از ریداکس و در سرویس‌های منطقی قرار دهیم که داده‌ها را به اکشن‌ها ارسال می‌کنند.

زمانی که منطق خود را به خارج از ردیوسر منتقل کنید، می‌توانید شروع به بهینه‌سازی بکنید. بدین ترتیب می‌توان store را به صورت «تغییرناپذیر» (Immutable) و «خالص» (Pure) درآورد، یعنی هر چیزی که حالت دریافت می‌کند را باید تنظیم کند و هیچ عارضه جانبی یا فراخوانی تابع دیگری ندارد. البته امکان اعمال قالب‌بندی وجود دارد، اما تغییر در ساختار ممکن نیست. توجه داشته باشید که ساختار باید قابل پیش‌بینی و قطعی باشد و دلیل تعیین یک حالت اولیه هم جز این نیست.

با پیروی از این قواعد می‌توان از کتابخانه‌های تغییرناپذیری در ردیوسر صرفاً برای به‌روزرسانی مقادیری که در store تغییر یافته‌اند استفاده کرد. این امری بسیار مهم است، زیرا امکان بهینه‌سازی بیشتر را در بقیه بخش‌های اپلیکیشن فراهم می‌سازد.

برای نمونه در ری‌اکت PureComponents را داریم که بر مبنای تفاوت‌های حالت که به کامپوننت ارسال می‌شوند بهینه‌سازی می‌کند. اگر هر بار صرفاً یک state کاملاً کلون شده را در ردیوسر می‌خواستیم، استفاده از PureComponents دیگر توجیهی نداشت. در این حالت بدون استفاده از PureComponents امکان memoize هم به صورت صحیح پدید نمی‌آید. بدین ترتیب می‌بینید که استفاده از تغییرناپذیری یک زنجیره از مزایا را به همراه دارد.

اینک ردیوسر بدون بهینه‌سازی به صورت زیر است:

1export const userReducer = (state = {}, action = {}) => {
2 
3  let newState = _.cloneDeep(state);
4  switch (action.type) {
5    case GET_USER:
6      newState.status = "runnning";
7      return newState; 
8  
9    case USER_SUCCESS: {
10      if(newState.user.name === undefined){
11        newState.user.name = "noname";
12      }
13      if(newState.user.time){
14        newState.timestamp = Date.now();
15        resetTimer(user.time)
16      }
17      return newState;
18    
19     case USER_ERROR:
20       newState.status = "error"
21       return newState;
22     default: {
23       return newState;
24     }
25  }
26};

در مثال بهینه‌سازی نشده فوق، چند مشکل وجود دارد. همچنان که می‌بینید هیچ حالت اولیه تعیین نشده است، از طرفی حالت deepCloning شده که عوارض جانبی دارد. همچنین store به صورت شرطی به‌روزرسانی می‌شود. همچنان که پیش‌تر اشاره شد این عارضه‌های جانبی بر روی امکان بهینه‌سازی تأثیر می‌گذارند و موجب می‌شود که قابلیت پیش‌بینی‌پذیری داده‌ها کاهش یابد.

در ادامه مثالی از ردیوسر خالص/تغییرناپذیر را در وضعیت بهینه‌سازی شده می‌بینید:

1import immutable from "immutability-helper";
2import {handleActions} from "redux-actions";
3const initialState = {
4  status: "",
5  user: {},
6};
7export const userReducer = handleActions(
8  {
9    [ACTIONS.GET_USER]: state =>
10      immutable(state, {
11        status: {$set: "running"}
12      }),
13    [ACTIONS.USER_SUCCESS]: (state, {data}) =>
14      immutable(state, {
15        status: {$set: "success"},
16        user: {$set: data.user},
17      }),
18    [ACTIONS.USER_ERROR]: state =>
19      immutable(state, {
20        status: {$set: "error"}
21      })
22  },
23  initialState
24);

در این نمونه بهینه‌سازی شده حجم کد کاهش یافته و اینک حالت پیش‌بینی‌پذیری بهتری دارد.

مزیت‌های این بهینه‌سازی به شرح زیر هستند:

  1. هیچ منطق یا عوارض جانبی وجود ندارد.
  2. مسئولیت منفردی تعریف شده است.
  3. همواره ساختار store یکسانی بازگشت می‌یابد.
  4. حالت اولیه برای افزایش انسجام/پیش‌بینی‌پذیری تعریف شده است.
  5. تغییرناپذیری کمک می‌کند صرفاً روی مقادیر تغییر یافته store عملیات کنیم.
  6. امکان کاربرد PureComponents و Memoizing فراهم شده است.

بدین ترتیب به پایان این مقاله می‌رسیم.

اگر این مطلب برای شما مفید بوده است، آموزش‌های زیر نیز به شما پیشنهاد می‌شوند:

==

بر اساس رای ۱ نفر
آیا این مطلب برای شما مفید بود؟
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
javascript-in-plain-english
نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *