ریداکس یک کتابخانه مدیریت حالت برای جاوااسکریپت است، که معمولا همراه کتابخانههایی مثل 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 "react" 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 خود را درون آن ایجاد میکنیم.
import { State } from ".." import { CounterAction } from "../actions" 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 تغییر اعمال میکند. یک منطق ساده دارد و آن هم این است که اگر دستور افزودن رسیده به شمارنده اضافه کن و دستور کاهش رسید از شمارنده کم کن!
برای استفاده از state و reducer که تعریف کردیم باید یک store ایجاد کنیم، تا کامپوننتها با مراجعه به آن مقدار state ها را دریافت کنند و به کاربر نمایش دهند. به سراغ فایل store/index.ts میرویم :
محتوای فایل store/index.ts بصورت زیر است :
import { Store, legacy_createStore as createStore } from "redux" import reducer from "./reducer" 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 ما آماده استفاده است.
به سراغ کامپوننت b که دکمه های افزایش و کاهش ما در آن قرار دارد، میرویم. وقتی کاربر روی دکمه Add کلیک کرد اکشن ADD_TO_COUNTER به reducer ارسال شود و اگر کاربر روی دکمه minus کلیک کرد، اکشن MINUS_FROM_COUNTER به reducer ارسال شود یا اصطلاحا dispatch شود. پس برای هر دکمه تابع مربوط به آن را تغییر میدهیم.
ابتدا store که ایجاد کرده بودیم را برای استفاده در کامپوننت b فراخوانی میکنیم :
import store from "../store"
اکنون میتوانیم از آن استفاده کنیم.
برای دکمه 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 "react" import store from "../store" import { addToCounter, minusFromCounter } from "../store/actions" 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;
نتیجه کار تا اینجا به این صورت هست که کاربر با زدن دکمه جمع یا منها هیچ چیزی در خروجی مشاهده نمیکند.
برای اینکه بتوانیم نتیجه را به کاربران نمایش دهیم، باید از تابع 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|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 میشوند.
این پست درون وبسایت شخصی من منتشر شده است.