ایجاد یک برنامهی خالی React برای آزمایش توابع Redux
در اینجا برای بررسی توابع Redux، یک پروژهی جدید React را ایجاد میکنیم:
> npm i -g create-react-app > create-react-app state-management-redux-mobx > cd state-management-redux-mobx > npm start
> npm install --save redux
معرفی سریع توابع Redux
Redux، کتابخانهی کوچکی است و تنها از 5 تابع تشکیل شدهاست:
applyMiddleware: function() bindActionCreators: function() combineReducers: function() compose: function() createStore: function()
بررسی تابع compose با یک مثال
پس از ایجاد پروژهی React و افزودن کتابخانهی Redux به آن، به فایل src\index.js این پروژه مراجعه کرده و محتویات آنرا با قطعه کد ذیل، تعویض میکنیم:
import { compose } from "redux"; const makeLouder = text => text.toUpperCase(); const repeatThreeTimes = text => text.repeat(3); const embolden = text => text.bold(); const makeLouderAndRepeatThreeTimesAndEmbolden = compose( embolden, repeatThreeTimes, makeLouder ); console.log(makeLouderAndRepeatThreeTimesAndEmbolden("Hello"));
- سپس سه تابع ساده را برای ضخیم کردن، تکرار و با حروف بزرگ نمایش دادن یک متن ورودی، تعریف کردهایم.
- اکنون با استفاده از متد compose کتابخانهی redux، این سه متد را به صورت ترکیبی، بر روی متن ورودی Hello، اعمال کردهایم.
- در آخر اگر برای مشاهدهی نتیجهی اجرای console.log بر روی آن، به کنسول توسعه دهندگان مرورگر مراجعه کنیم، به خروجی زیر خواهیم رسید:
<b>HELLOHELLOHELLO</b>
بررسی تابع createStore با یک مثال
Store در Redux، جائی است که در آن state برنامه ذخیره میشود. تابع createStore که کار ایجاد store را انجام میدهد، یک پارامتر را دریافت میکند و آنهم تابع reducer است که در قسمت قبل معرفی شد و در سادهترین حالت آن، به نحو زیر با یک متد خالی، قابل فراخوانی است:
import { createStore } from "redux"; createStore(() => {});
import { createStore } from "redux"; const reducer = (state, action) => { return state; }; const store = createStore(reducer); console.log(store);
در شیء store، چهار متد dispatch, subscribe, getState, replaceReducer مشخص هستند:
- کارکرد متد replaceReducer مشخص است؛ یک reducer جدید را به آن میدهیم و reducer قبلی را جایگزین میکند.
- متد dispatch آن مرتبط است به مبحث dispatch actions که در قسمت قبل به آن پرداختیم. هدف آن تغییر state، بر اساس یک action رسیدهاست.
- متد getState، وضعیت فعلی state را باز میگرداند.
- متد subscribe با هر تغییری در state، سبب بروز رخدادی میشود. یکی از کاربردهای آن میتواند به روز رسانی UI، بر اساس تغییرات state باشد. برای مثال کتابخانهی دیگری به نام react redux، دقیقا همین کار را به کمک متد subscribe، انجام میدهد. در این قسمت، هدف ما بررسی پشت صحنهی کتابخانههایی مانند react redux است که چه متدهایی را محصور کردهاند و دقیقا چگونه کار میکنند.
در مثال زیر، مقدار ابتدایی پارامتر state متد reducer را به یک شیء که دارای خاصیت value و مقدار 1 است، تنظیم کردهایم:
import { createStore } from "redux"; const reducer = (state = { value: 1 }, action) => { return state; }; const store = createStore(reducer); console.log(store.getState());
در کامپوننتهای React، این نوع خواص به صورت props ارسال میشوند. کل state در Redux ذخیره شده و سپس قابل دسترسی و خواندن خواهد شد.
بررسی متد dispatch یک store با مثال
در اینجا مثالی را از کاربرد متد dispatch ملاحظه میکنید که یک شیء را میپذیرد:
import { compose, createStore } from "redux"; const reducer = (state = { value: 1 }, action) => { console.log("Something happened!", action); return state; }; const store = createStore(reducer); console.log(store.getState()); store.dispatch({ type: "ADD" });
فرمت شیءای که به متد dispatch ارسال میشود، حتما باید به همراه خاصیت type باشد؛ در غیر اینصورت با صدور استثنائی، این نکته را گوشزد میکند. مقدار آن نیز یک رشتهاست که بیانگر وقوع رخدادی در برنامه میباشد؛ برای مثال کاربر درخواست دریافت اطلاعاتی را کردهاست و یا کاربر از سیستم خارج شدهاست و امثال آن.
خروجی قطعه کد فوق، به صورت زیر است:
با اجرای برنامه، یک رخداد درونی از نوع redux/INITo.2.g.b.y.i@@ صادر شدهاست که هدف آن آغاز و مقدار دهی اولیهی store است. پس از آن زمانیکه متد store.dispatch فراخوانی شدهاست، مجددا console.log داخل متد reducer فراخوانی شدهاست که اینبار به همراه type ای است که ما مشخص کردهایم.
در حالت کلی، اینکه شیء ارسالی توسط dispatch را چگونه طراحی میکنید، اختیاری است؛ اما الگوی پیشنهادی در این زمینه، redux standard actions نام دارد و به صورت زیر است که هدف از آن، یکدست شدن طراحی و پیاده سازی برنامه است:
store.dispatch({ type: "ADD", payload: { amount: 2 }, meta: {} });
- سپس به خاصیت payload، تمام دادههای مرتبط با آن اکشن، مانند نتیجهی بازگشتی از یک API، به صورت یک شیء، انتساب داده میشود.
- خاصیت meta، مرتبط با متادیتا و اطلاعات اضافی است که عموما استفاده نمیشود.
اکنون که طراحی شیء ارسالی به پارامتر action متد reducer مشخص شد، میتوان بر اساس خاصیت type آن، یک switch را طراحی کرد و نسبت به نوعهای مختلف رسیده، واکنش نشان داد:
import { createStore } from "redux"; const reducer = (state = { value: 1 }, action) => { console.log("Something happened!", action); if (action.type === "ADD") { const value = state.value; const amount = action.payload.amount; state.value = value + amount; } return state; }; const store = createStore(reducer); const firstState = store.getState(); console.log(firstState); store.dispatch({ type: "ADD", payload: { amount: 2 }, meta: {} }); const secondState = store.getState(); console.log(secondState); console.log("secondState === firstState", secondState === firstState);
یک روش حل این مشکل، حذف سطر state.value = value + amount و جایگزینی آن با یک return است که شیء state جدیدی را بازگشت میدهد:
return { value: value + amount };
بررسی متد subscribe یک store با مثال
در ادامه به store همان مثال فوق، متد subscribe را نیز اضافه میکنیم و سپس دوبار، کار store.dispatch را انجام خواهیم داد:
const store = createStore(reducer); const unsubscribe = store.subscribe(() => console.log(store.getState())); store.dispatch({ type: "ADD", payload: { amount: 2 }, meta: {} }); store.dispatch({ type: "ADD", payload: { amount: 2 }, meta: {} }); unsubscribe();
- خروجی مستقیم متد store.subscribe نیز یک متد است که unsubscribe نام دارد و میتوان در پایان کار، برای جلوگیری از نشتیهای حافظه، آنرا فراخوانی کرد.
خروجی حاصل از console.log منتسب به متد store.subscribe، با دوبار فراخوانی متد store.dispatch پس از آن، به صورت زیر است:
{value: 3} {value: 5}
بررسی تابع combineReducers با یک مثال
اگر قرار باشد در متد reducer، صدها if action.type را تعریف کرد ... پس از مدتی از کنترل خارج میشود و غیرقابل نگهداری خواهد شد. تابع combineReducers به همین جهت طراحی شدهاست تا چندین متد مجزای reducer را با هم ترکیب کند. بنابراین میتوان در برنامه صدها متد کوچک reducer را که قابلیت توسعه و نگهداری بالایی را دارند، پیاده سازی کرد و سپس با استفاده از تابع combineReducers، آنها را یکی کرده و به متد createStore ارسال کرد. نکتهی مهم اینجا است که هرچند اینبار میتوان تعداد زیادی reducer را طراحی کرد، اما توسط تابع combineReducers، مقدار action ارسالی، از تمام این reducerها عبور داده خواهد شد. به این ترتیب اگر نیاز بود میتوان به یک action، در چندین متد مختلف reducer گوش فرا داد و بر اساس آن تصمیم گیری کرد. بنابراین بهتر است هر reducer تعریف شده در صورتیکه action.type آن با action رسیده تطابق نداشته باشد، همان state قبلی را بازگشت دهد تا بتوان زنجیرهی reducerها را تشکیل داد و بهتر مدیریت کرد.
برای نمونه اگر متد reducer فعلی را به calculatorReducer تغییر نام دهیم، روش معرفی آن توسط متد combineReducers به متد createStore به صورت زیر است:
import { combineReducers, createStore } from "redux"; // ... const calculatorReducer = (state = { value: 1 }, action) => { // ... }; const store = createStore( combineReducers({ calculator: calculatorReducer }) );
بررسی تابع bindActionCreators با یک مثال
فرض کنید میخواهیم متد dispatch را چندین بار فراخوانی کنیم:
store.dispatch({ type: "ADD", payload: { amount: 2 }, meta: {} }); store.dispatch({ type: "ADD", payload: { amount: 2 }, meta: {} });
const createAddAction = amount => { return { type: "ADD", payload: { amount // = amount: amount }, meta: {} }; };
store.dispatch(createAddAction(2)); store.dispatch(createAddAction(2));
import { bindActionCreators, combineReducers, compose, createStore } from "redux"; // ... const dispatchAdd = bindActionCreators(createAddAction, store.dispatch); dispatchAdd(2); dispatchAdd(2);
میانافزارها (Middlewares) در Redux
پس از اینکه یک اکشن، به سمت reducer ارسال شد و پیش از رسیدن آن به reducer، میتوان کدهای دیگری را نیز اجرا کرد. برای مثال چون این توابع خالص هستند، نمیتوان اعمالی را داخل آنها اجرا کرد که به همراه اثرات جانبی مانند کار با یک API خارجی باشند. با استفاده از میانافزارها در این بین میتوان کدهایی را که با دنیای خارج تعامل دارند نیز اجرا کرد.
یک میانافزار در Redux، همانند سایر قسمتهای آن، تنها یک تابع سادهی جاوا اسکریپتی است:
const logger = ({ getState }) => { return next => action => { console.log( 'MIDDLEWARE', getState(), action ); const value = next(action); console.log({value}); return value; } }
در پایان کار میانافزار باید متد next آنرا فراخوانی کرد تا بتوان میانافزارهای متعددی را زنجیروار اجرا کرد.
در آخر برای معرفی آن به یک store میتوان از تابع applyMiddleware استفاده کرد:
import { applyMiddleware, bindActionCreators, combineReducers, compose, createStore } from "redux"; // ... const store = createStore( combineReducers({ calculator: calculatorReducer }), applyMiddleware(logger) );
کدهای کامل این قسمت را از اینجا میتوانید دریافت کنید: state-management-redux-mobx-part02.zip