عباس باقری
عباس باقری
خواندن ۱۰ دقیقه·۲ سال پیش

مدیریت حالات برنامه با ریداکس

ریداکس یک کتابخانه مدیریت حالت برای جاوااسکریپت است، که معمولا همراه کتابخانه‌هایی مثل reactjs استفاده میشود.

ریداکس بهترین روش نیست ولی خب بدترین هم نیست. در برخی پروژه ها استفاده از ریداکس منطقی هم نیست. اجباری هم نیست که حتما باید از redux استفاده کنیم، روشهای دیگری هم وجود دارد که همین کار را میکند، شما خودتان هم میتوانید یک state manager جدید پیاده سازی کنید. اما واقع بین باشیم حتی اگر خوشمان نیاید ولی کسی بابت خوشایند ما به ما پول نمیده. اگر میخواهیم برنامه نویس فرانت شویم و از دیگران پول بگیریم، باید با redux هم آشنا باشیم.

هدف از استفاده از ریداکس چیست؟

در وب اپلیکیشنها state زیادی در کامپوننتهای زیادی داریم، اکثر این state ها ارتباط مستقیمی با هم ندارند. ولی خب state هایی هم هستند که به یکدیگر ارتباط پیدا میکنند. بگذارید مثال سبد خرید فروشگاه آنلاین را بزنیم. در این مثال، وقتی کاربر روی دکمه سبد خرید کلیک میکند state دکمه افزودن به سبد خرید باید تغییر کند مثلا رنگ دکمه و متنش تغییر کند. state تعداد محصولات خرید هم باید درون هدر سایت تغییر کند و همچنین درون خود سبد خرید هم باید لیست محصولات تغییر کند. پس تا اینجا حداقل سه تا کامپوننت مجزا داریم که یک state مشترک دارند. و آن هم لیست سبد خرید است. هندل کردن این اتفاق بدون ریداکس کار سختی نیست ولی با ریداکس این کار آسانتر میشود. به عبارتی ما state لیست خرید را در store ریداکس تعریف میکنیم و درون هر کامپوننتی که بخواهیم به آن دسترسی پیدا میکنیم و آنرا تغییر میدهیم.

بگذارید روی مثال خودمان متمرکز شویم

شمارنده بدون ریداکس
شمارنده بدون ریداکس

همونطور که درون تصویر مشخصه سه تا کامپوننت داریم. state مشترک بین این سه کامپوننت counter هست. از این به بعد بهش میگیم شمارنده.

کامپوننت A :

import { useState } from 'react'; export default function CompnentA(){ const [count,setCount] = useState(0); return (<h3>Counter State in A : {count}</h3>); }

کامپوننت B :

import React from &quotreact&quot class CompnentB extends React.Component{ state = {counter:0,} render(): React.ReactNode { return (<> <h3>Counter in Component B : {this.state.counter}</h3> <button ={()=>this.setState({counter:this.state.counter+1})}>Add</button> <button ={()=>this.setState({counter:this.state.counter-1})}>Minus</button> </>); } } export default CompnentB;

همینطور که میبینید هرکدام از دکمه ها کلیک شود مقدار شمارنده یک واحد تغییر میکند.

کامپوننت C:

import { useState } from 'react'; export default function CompnentC(){ const [count,setCount] = useState(0); return (<h3>Counter State in A : {count}</h3>); }

این کامپوننت مشابه A مقدار شمارنده را باید نمایش دهد.

اکنون ما سه کامپوننت داریم که میخواهیم مقدار فعلی شمارنده را در هر کدام نمایش دهیم. بدون ریداکس state های کامپوننتها را باید به یکدیگر پاس دهیم. اما میریم سراغ کار خودمان یعنی ریداکس...




ریداکس چگونه کار میکند؟

ممکن است مفاهیمی که من استفاده میکنم با مفاهیم رسمی متفاوت باشد اما صرفا جهت ساده سازی از برخی مفاهیم نابجا ممکن است استفاده کنم.

حالت یا state : در پروژه شمارنده، state مشترک بین چند کامپوننت، counter است.

بجای اینکه هر state را درون هر کامپوننت بصورت محلی تعریف کنیم، یک state سراسری برای ریداکس تعریف میکنیم و همه کامپوننتهایی که این state را لازم دارند میتوانند آن را از store ریداکس دریافت کنند.


حالت شروع یا initialState : همین طور که ما درون هر کامپوننت یک state با مقدار اولیه تعریف میکنیم، باید برای redux هم یک حالت شروع تعریف کنیم. مثلا درون پروژه شمارنده state اولیه بصورت زیر است.

interface State { counter:number; } const initialState:State = { counter:0, }


محل عرضه یا store : کامپوننتهای مختلف از طریق یک واسط همانند فروشگاه به state های ریداکس دسترسی دارند. مثلا کامپوننت a از طریق store مقدار state شمارنده را درخواست کند.

دنبال کردن یا subscribe : طرفداران اپل که همیشه اخبار این شرکت را دنبال میکنند و سریعا با عرضه محصول جدید، آن را تهیه میکنند. بحث subscribe درون ریداکسن هم تقریبا همین است. کامپوننتها همواره store را دنبال میکنند یا اصطلاحا subscribe میکنند و همان لحظه که state تغییر کرد مقدار آن را دریافت و نمایش میدهند.

اکشن یا action : دستورالعملی که برای ریداکس ارسال میشود. مثلا وقتی کاربر روی دکمه add در پروژه شمارنده کلیک میکند، در حقیقت یک action برای redux برای ریداکس ارسال میشود که به state شمارنده را اضافه کن یا وقتی دکمه minus کلیک شود دستورالعملی برای کاهش مقدار state شمارنده ارسال می‌کند.

اعمال کننده یا reducer : در بحث ریداکس، یک بخش مهم داریم بنام reducer و وظیفه اش هم خیلی ساده است. اکشن را از کاربر و state قبلی را دریافت میکند و برروی state ها تغییرات را بر اساس آن اکشن اعمال میکند و در نهایت state جدید را تحویل فروشگاه یا store برای عرضه به مشتریها که همان کامپوننتها باشد میدهد.

ارسال یا dispatch : منظور از dispatch در بحث ریداکس ، ارسال اکشن به reducer است.


احتمالا بحثها برای کسی که تابه حال با ریداکس کار نکرده باشد کمی گنگ به نظر برسد. به تصویر زیر دقت کنید :

عملکرد ریداکس
عملکرد ریداکس

وقتی کاربر روی دکمه ای کلیک کرد یک اکشن (دستورالعمل) به reducer ارسال میشود. reducer هم برحسب وظیفه state قبلی را از فروشگاه میگیرد و براساس action دریافتی، تغییرات جدید را را روی state اعمال میکند و به فروشگاه برای عرضه تحویل میدهد. کامپوننتها هم همواره منتظر هستند که state تغییر کند تا سریعتر آنرا دریافت کنند. مثلا مقدار شمارنده 1 شد، همه کامپوننتها مقدار 1 را دریافت میکنند.

امیدوارم خوب و کافی قسمت تئوری را توضیح داده باشم.


شروع کار با ریداکس

ابتدا باید ریداکس را به پروژه خود اضافه کنیم :

npm i redux

روش مرسوم این است درون پروژه خود یک دایرکتوری به نام store ایجاد میکنیم و فایل index را به آن اضافه میکنیم :

سپس درون این فولدر یک فولدر دیگر برای action ها و یک فولدر هم برای reducer ها اضافه میکنیم :

به سراغ فایل index موجود درون store میرویم و initialeState را تعریف میکنیم :

interface State { counter:number; } export const initialState:State = { counter:0, } export type {State}


ایجاد دستورالعملها

مرسوم است ابتدا action هایی را برای ریداکس تعیین کنیم. مثلا در پروژه شمارنده، دو action داریم. یکی اضافه کردن به شمارنده و دیگری کاهش مقدار شمارنده.

interface CounterAction { type: 'ADD_TO_COUNTER' | 'MINUS_FROM_COUNTER'; payload: { value:number } } export const addToCounter:CounterAction = { type:'ADD_TO_COUNTER', payload : { value:1, } } export const minusFromCounter:CounterAction = { type:'MINUS_FROM_COUNTER', payload : { value:1, } }

ما دو اکشن MINUS_FROM_COUNTER و ADD_TO_COUNTER را در فایل actions/index.ts تعریف کردیم. در هر اکشن یک type داریم یک payload. ویژگی type را حتما باید مقداردهی کنیم. مقداری که برای type در نظر میگیریم مثل id برای این اکشن است. و باید مختص خودش باشد. نمیتوانیم دو اکشن با type یکسان داشته باشیم.

در عوض مقداردهی و استفاده از payload اختیاری است. مثلا من در payload مقدار value برابر با یک را قرار دادم تا درون reducer از آن استفاده کنم.

چیز عجیبی نیست هر اکشن یک object است که پراپرتی type برای آن اجباری و payload هم اختیاری است.


ایجاد reducer

پس از اینکه نوع اکشنهای موجود در برنامه خود را تعیین کردیم نوبت به reducer میشود. به سراغ فولدر reducer می رویم و reducer خود را درون آن ایجاد میکنیم.

import { State } from &quot..&quot import { CounterAction } from &quot../actions&quot function reducer(state:State,action:CounterAction):State{ const currentCounter = state.counter; switch(action.type){ case 'ADD_TO_COUNTER': return {...state,counter:currentCounter + action.payload.value} case 'MINUS_FROM_COUNTER': return {...state,counter:currentCounter - action.payload.value} default: return state; } } export default reducer;

همینطور که میبینید reducer هم چیز عجیبی نیست. یک تابع است که state قبلی و action کاربر را دریافت میکند و بر اساس نوع اکشن یک عملیات روی state، که اینجا کم کردن یا افزودن به آن است، انجام میدهد و state جدید را برگشت میدهد.

خیلی ساده بود. شاید بخاطر همین سادگی پیچیده بنظر برسد ولی در حقیقت خیلی ساده بود. اکشن صرفا یک object ساده است و reducer هم صرفا یک تابع که action را میگیرد و روی state تغییر اعمال میکند. یک منطق ساده دارد و آن هم این است که اگر دستور افزودن رسیده به شمارنده اضافه کن و دستور کاهش رسید از شمارنده کم کن!


تعریف store

برای استفاده از state و reducer که تعریف کردیم باید یک store ایجاد کنیم، تا کامپوننتها با مراجعه به آن مقدار state ها را دریافت کنند و به کاربر نمایش دهند. به سراغ فایل store/index.ts میرویم :

محتوای فایل store/index.ts بصورت زیر است :

import { Store, legacy_createStore as createStore } from &quotredux&quot import reducer from &quot./reducer&quot interface State { counter:number; } export const initialState:State = { counter:0, } const store:Store = createStore( reducer, //reducer initialState, //state ); export default tore; export type {State}


برای ساخت store مورد نیاز باید از تابع createStore استفاده کنیم. این تابع در ورودی reducer و initialState را دریافت میکند و یک store آماده استفاده به ما تحویل میدهد.

دقت کنید تابع createStore دیگر پشتیبانی نمیشود، بجای آن میتوان تابع legacy_createStore را استفاده کرد.

اکنون store ما آماده استفاده است.


ارسال اکشن به reducer

به سراغ کامپوننت b که دکمه های افزایش و کاهش ما در آن قرار دارد، میرویم. وقتی کاربر روی دکمه Add کلیک کرد اکشن ADD_TO_COUNTER به reducer ارسال شود و اگر کاربر روی دکمه minus کلیک کرد، اکشن MINUS_FROM_COUNTER به reducer ارسال شود یا اصطلاحا dispatch شود. پس برای هر دکمه تابع مربوط به آن را تغییر میدهیم.

ابتدا store که ایجاد کرده بودیم را برای استفاده در کامپوننت b فراخوانی میکنیم :

import store from &quot../store&quot

اکنون میتوانیم از آن استفاده کنیم.

برای دکمه add تابعی به همین نام تعریف میکنیم. و درون آن اکشن ADD_TO_COUNTER را dispach میکنیم :

add(){ store.dispatch(addToCounter); }

برای دکمه minus هم باید اکشن minusFromCounter را dispatch کنیم :

minus(){ store.dispatch(minusFromCounter) }


تابع dispatch در ورودی یک اکشن دریافت میکند و آنرا برای reducer ارسال میکند. reducer هم بر اساس type این اکشن با state رفتار میکند.

محتوای componentB به زیر تغییر کرد :
import React from &quotreact&quot import store from &quot../store&quot import { addToCounter, minusFromCounter } from &quot../store/actions&quot class CompnentB extends React.Component{ state = {counter:0,} add(){ store.dispatch(addToCounter); } minus(){ store.dispatch(minusFromCounter) } render(): React.ReactNode { return (<> <h3>Counter in Component B : {this.state.counter}</h3> <button ={this.add}>Add</button> <button ={this.minus}>Minus</button> </>); } } export default CompnentB;

نتیجه کار تا اینجا به این صورت هست که کاربر با زدن دکمه جمع یا منها هیچ چیزی در خروجی مشاهده نمیکند.


نمایش state به کاربران

برای اینکه بتوانیم نتیجه را به کاربران نمایش دهیم، باید از تابع subscribe استفاده کنیم. به سراغ componentDidMount در کامپوننت b می رویم :

componentDidMount() { store.subscribe(()=>{ const state = store.getState(); this.setState({counter:state.counter}); }); }

تابع subscribe در ورودی یک تابع میگیرد و هر وقت مقدار state ریداکس تغییر کرد، آن تابع اجرا میشود. مثلا در این مثال وقتی کاربر دکمه add یا minus را فشار داد، مقدار counter کامپوننتB تغییر میکند و دقیقا همان مقدار counter موجود در redux state میشود.

مثلا در شکل بالا، وقتی کاربر دکمه add را فشار داد، اکشن ADD_TO_COUNTER به وسیله dispatch به reducer میرسد و reducer مقدار state را تغییر میدهد. با تغییر state در ریداکس، state شمارنده در کامپوننت B هم در لحظه تغییر خواهد کرد.

تازه رسیدیم به اول کار، یعنی موقعی که componentB مقدار شمارنده را نمایش میداد برای سایر کامپوننت ها باید از subscribe استفاده کنیم. به سراغ کامپوننت A میرویم :

در کامپوننتهای مبتنی بر کلاس باید در componentDidMount تابع subscribe را فراخوانی کنیم و درون کامپوننتهای مبتنی بر تابع، با هوک useEffect.


چگونه unsubscribe کنیم؟

unsubscribe:Unsubscribe|null=null; componentDidMount() { this.unsubscribe = store.subscribe(()=>{ const state = store.getState(); this.setState({counter:state.counter}); }) }

وقتی تابع subscribe را اجرا کنیم در خروجی تابع unsubscribe را برمیگرداند که میتوانیم از آن در بخشهای مختلف برنامه برای عمل unsubscribe در صورت لزوم استفاده کنیم.

خلاصه مطلب

کامپوننتها store را subscribe کرده اند و همیشه به state آن نگاه میکنند و با آپدیت state ریداکس state هر کامپوننت نیز آپدیت میشود. و در نتیجه همیشه همه کامپوننتها مقدار یکسان counter را نمایش میدهند.

وقتی کاربر روی یک دکمه کلیک میکند، یک اکشن dispatch میشود و به reducer فرستاده میشود. reducer هم بر اساس type اکشن مقدار state را تغییر میدهد و به store میفرستد. به محض تغییر state هم کامپوننتها rerender میشوند.

این پست درون وبسایت شخصی من منتشر شده است.

ادامه این مطلب در ویرگول

سبد خریدریداکسreduxجاوااسکریپتreactjs
برنامه نویس و طراح وب‌سایت
شاید از این پست‌ها خوشتان بیاید