در دنیای همیشه در حال توسعه توسعه وب، کتابخانه های جدید جاوا اسکریپت همیشه منتشر می شوند. اما تعقیب هر نسخه جدید بدون درک کامل مزایای آن ایده خوبی نیست. Redux نمونه ای از کتابخانه جاوا اسکریپت است که محبوبیت پایدار آن گواهی بر ارزش آن است. Redux یک کانتینر state قابل پیش بینی برای برنامه های جاوا اسکریپت است. پس واقعاً به چه معناست؟ اگر عمیقتر به این عبارت بپردازیم، میبینیم که Redux یک کتابخانه مدیریت state است که میتوانید با هر کتابخانه یا چارچوب JS مانند React، Angular یا Vue از آن استفاده کنید.
در این مقاله، ما اصول اولیه Redux را پوشش خواهیم داد. ما یاد خواهیم گرفت که Redux در هسته خود به همراه سه اصل کلیدی آن چیست.
همچنین خواهیم دید که برخی از بلوکهای اصلی آن، مانند Store, Actions و Reducers چگونه کار میکنند و چگونه همه آنها با هم جمع میشوند و Redux را به کتابخانه مدیریت سراسری state تبدیل میکنند.
Redux چیست؟
Redux یک کانتینر state قابل پیشبینی است که به شما کمک میکند تا برنامههای جاوا اسکریپت بنویسید که به طور مداوم در بین کلاینت، سرور و محیطهای بومی رفتار میکنند و آزمایش آن آسان است.
در حالی که بیشتر به عنوان یک ابزار مدیریت state با React استفاده می شود، می توانید از Redux با هر چارچوب یا کتابخانه دیگری جاوا اسکریپت استفاده کنید. وزن آن 2 کیلوبایت (شامل وابستگی ها) است، بنابراین لازم نیست نگران بزرگتر کردن اندازه asset برنامه خود باشید.
با ریداکس، state برنامه شما در یک فروشگاه یا همان Store نگهداری می شود و هر کامپوننت می تواند به هر state ی که نیاز دارد از این فروشگاه دسترسی داشته باشد.
زمان استفاده از Redux
مدت زیادی پس از انتشار، Redux تبدیل به یکی از داغ ترین موضوعات بحث در دنیای frontend شد. ولی برای مبتدی ها و علاقه مندان سوال باشد که چه زمانی باید از آن استفاده کرد؟!
Redux به شما امکان می دهد state های برنامه خود را در یک مکان واحد مدیریت کنید و تغییرات در برنامه خود را قابل پیش بینی تر و قابل ردیابی تر نگه دارید و درک تغییراتی که در برنامه شما اتفاق می افتد را آسان تر می کند. اما همه این مزایا با مجموعه ای از چالش ها همراه است. برخی از توسعه دهندگان استدلال می کنند که ریداکس boilerplate غیر ضروری را معرفی می کند که به طور بالقوه کارهای ساده ای را پیچیده می کند. با این حال، این به تصمیمات معماری پروژه بستگی دارد.
بنابراین، چه زمانی باید از Redux استفاده کنید؟ یک پاسخ ساده به این سوال این است که شما به طور ارگانیک متوجه خواهید شد که چه زمانی به Redux نیاز دارید. اگر مطمئن نیستید که آیا به آن نیاز دارید، احتمالاً ندارید. این معمولاً زمانی اتفاق میافتد که برنامه شما به مقیاسی برسد که مدیریت state برنامه به یک دردسر تبدیل شود و شما شروع به جستجوی راههایی برای سادهسازی آن کنید.
چرا از Redux استفاده کنیم؟
خب یک اپلیکیشن state خود را دارد که می تواند ترکیبی از حالات اجزای داخلی آن باشد.
بیایید به عنوان مثال یک وب سایت تجارت الکترونیک را در نظر بگیریم. یک وب سایت تجارت الکترونیک دارای چندین کامپوننت مانند کامپوننت سبد خرید، کامپوننت پروفایل کاربر، کامپوننت بخش قبلاً مشاهده شده و غیره است.
ما کامپوننت سبد خرید را می گیریم که تعداد اقلام موجود در سبد خرید کاربر را نشان می دهد. state کامپوننت سبد خرید شامل تمام مواردی است که کاربر به سبد خرید اضافه کرده است و تعداد کل آن موارد. در همه زمانهایی که برنامه راهاندازی و اجرا میشود، این کامپوننت باید تعداد بهروزشده اقلام موجود در سبد خرید کاربر را نشان دهد.
هر زمان که کاربر موردی را به سبد خرید اضافه می کند، برنامه باید با افزودن آن کالا به شی سبد خرید، آن عمل را به صورت داخلی انجام دهد. باید state خود را در داخل حفظ کند و همچنین تعداد کل اقلام موجود در سبد خرید را در رابط کاربری به کاربر نشان دهد.
به همین ترتیب، حذف یک کالا از سبد خرید باید تعداد اقلام داخل سبد را کاهش دهد. باید مورد را از شی سبد خرید حذف کند و همچنین تعداد کل به روز شده اقلام موجود در سبد خرید را در رابط کاربری نمایش دهد.
ممکن است state داخلی اجزای داخل آنها را به خوبی حفظ کنیم، اما زمانی که یک برنامه بزرگتر می شود، ممکن است مجبور شود state ی را بین کامپوننت به اشتراک بگذارد. این فقط برای نشان دادن آنها در view نیست، بلکه برای مدیریت یا به روز رسانی آنها یا انجام برخی منطق بر اساس ارزش آنها است.
این وظیفه مدیریت چند state از چندین کامپوننت به طور مؤثر می تواند زمانی که برنامه بزرگ می شود چالش برانگیز شود.
اینجاست که Redux وارد بازی می شود. Redux به عنوان یک کتابخانه مدیریت state، اساساً تمام state های برنامه را ذخیره و مدیریت می کند.
همچنین برخی از API های مهم را در اختیار ما قرار می دهد که با استفاده از آنها می توانیم تغییراتی در state موجود ایجاد کنیم و همچنین state فعلی برنامه را واکشی کنیم.
چه چیزی Redux را قابل پیش بینی می کند؟
State در Redux فقط خواندنی است. چیزی که Redux را قابل پیشبینی میکند این است که برای ایجاد تغییر در state برنامه، باید اقدامی (همان Actions) را ارسال کنیم که توضیح دهد چه تغییراتی را میخواهیم در state ایجاد کنیم.
سپس این اقدامات توسط چیزی به نام کاهش دهنده ها مصرف می شود که تنها وظیفه آن پذیرش دو چیز (عمل و state فعلی برنامه) و بازگرداندن یک نمونه به روز شده جدید از state است.
در بخش های بعدی بیشتر در مورد اقدامات و کاهش دهنده ها صحبت خواهیم کرد.
توجه داشته باشید که کاهنده ها که همان Reducers است هیچ بخشی از state را تغییر نمی دهند. بلکه یک Reducer نمونه جدیدی از state را با تمام به روز رسانی های لازم تولید می کند.
خالق ریداکس می گوید:
"Actions را می توان بعدا ذخیره و دوباره پخش کرد، بنابراین این امر مدیریت state را قابل پیش بینی می کند. با همان Actions به همان ترتیب، شما در نهایت به همان state خواهید رسید."
بنابراین با ادامه مثال بالا در مورد یک وب سایت تجارت الکترونیک، اگر state اولیه سبد خرید این باشد که 0 مورد داشته باشد، آنگاه با افزودن یک کالا به سبد خرید، تعداد اقلام در سبد خرید 1 مورد افزایش می یابد. با اضافه کردن مجدد یک کالا به سبد خرید، تعداد اقلام در سبد خرید به 2 مورد افزایش می یابد.
با توجه به یک state اولیه، با لیست خاصی از Actions در یک ترتیب خاص، همیشه دقیقاً همان state نهایی موجودیت را در اختیار ما قرار می دهد. اینگونه است که Redux مدیریت state را قابل پیش بینی می کند.
در بخش بعدی، ما عمیقاً به مفاهیم اصلی ریداکس یعنی Store، Actions و Reducers می پردازیم.
اصول اصلی Redux
1. فروشگاه یا Redux Store چیست؟
بر اسا داکیومنت های ریداکس، state سراسری یک برنامه در یک درخت شی در یک فروشگاه ذخیره می شود.
فروشگاه Redux محل اصلی و مرکزی است که تمام state های یک برنامه را ذخیره می کند. باید به عنوان منبعی واحد از حقیقت برای state برنامه در نظر گرفته شود و حفظ شود.
اگر Store همانطور که در قطعه کد زیر نشان داده شده است در اختیار App.js
قرار گیرد (با قرار دادن کامپوننت App در تگ <Provider> </Provider>
)، آنگاه همه فرزندان آن (اجزای فرزند App.js
) نیز می توانند به آن دسترسی داشته باشند. state برنامه از Store این باعث می شود که به عنوان یک state سراسری عمل کند.
// src/index.js
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import { App } from './App'
import createStore from './createReduxStore'
const store = createStore()
// As of React 18
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
<Provider store={store}>
<App />
</Provider>
)
state کل برنامه به شکل درخت آبجکت JS در یک Store ذخیره می شود، همانطور که در زیر نشان داده شده است.
// this is how the store object structure looks like
{
noOfItemInCart: 2,
cart: [
{
bookName: "Harry Potter and the Chamber of Secrets",
noOfItem: 1,
},
{
bookName: "Harry Potter and the Prisoner of Azkaban",
noOfItem: 1
}
]
}
2. Actions در Redux چیست؟
با استناد بر دایکومنت ریداکس : تنها راه برای تغییر state، انتشار یک Actions است، که یک شی است که آنچه اتفاق افتاده را توصیف می کند.
همانطور که در بالا ذکر شد، state در Redux فقط خواندنی است. این به شما کمک میکند تا هر بخشی از view یا network calls را برای نوشتن/بهروزرسانی مستقیم state محدود کنید.
در عوض، اگر کسی بخواهد state برنامه را تغییر دهد، باید قصد خود را برای انجام این کار با ارسال یا dispatching یک Action بیان کند.
بیایید مثال فروشگاه فوق را مثال بزنیم که در آن 2 کتاب در فروشگاه داریم: "هری پاتر و تالار اسرار" و "هری پاتر و زندانی آزکابان". فقط یک کپی از هر کدام وجود دارد.
حال اگر کاربر بخواهد کالای دیگری را به سبد خرید اضافه کند، باید روی دکمه «افزودن به سبد خرید» در کنار کالا کلیک کند.
با کلیک بر روی دکمه "افزودن به سبد خرید"، یک عملیات ارسال می شود. این عمل چیزی نیست جز یک شی JS که توضیح می دهد چه تغییراتی باید در فروشگاه انجام شود.
چیزی شبیه به این:
// Rest of the code
const dispatch = useDispatch()
const addItemToCart = () => {
return {
type: "ADD_ITEM_TO_CART"
payload: {
bookName: "Harry Potter and the Goblet of Fire",
noOfItem: 1,
}
}
}
<button onClick = {() => dispatch(addItemToCart())}>Add to cart</button>
// Rest of the code
توجه داشته باشید که چگونه در مثال بالا، یک عمل را با کلیک روی دکمه ارسال می کنیم. یا به عبارت دقیقتر، چیزی را که به عنوان ایجاد کننده اکشن شناخته میشود، ارسال میکنیم – یعنی تابع ()addItemToCart
. این به نوبه خود Action را برمیگرداند که یک شیء JS ساده است که هدف عمل مشخص شده با کلید نوع را همراه با سایر دادههای مورد نیاز برای تغییر state توصیف میکند. در این state، نام کتابی است که باید به سبد خرید اضافه شود که با کلید بارگذاری مشخص شده است.
هر عملی باید حداقل یک نوع مرتبط با آن داشته باشد. هر جزئیات دیگری که نیاز به ارائه دارد اختیاری است و به نوع Action که ما ارسال می کنیم بستگی دارد.
به عنوان مثال، قطعه کد بالا عمل زیر را ارسال می کند:
// Action that got created by the action creator addItemToCart()
{
type: "ADD_ITEM_TO_CART" // Note: Every action must have a type key
payload: {
bookName: "Harry Potter and the Goblet of Fire",
noOfItem: 1,
}
}
3. کاهش دهنده ها یا Reducers در Redux چیست؟
طبق داکیومنت ریداکس برای مشخص کردن اینکه چگونه درخت state با اعمال تغییر شکل میدهد، reducers خالص را مینویسیم.
reducer ها، همانطور که از نام آن ها پیداست، دو چیز را انجام می دهند: state قبلی و یک عمل. سپس آن را کاهش می دهند (بخوانید آن را بازگشت) به یک entity: نمونه به روز شده جدید state.
بنابراین reducer ها اساساً توابع JS خالص هستند که در state قبلی و یک اکشن قرار می گیرند و state جدید به روز شده را برمی گردانند.
اگر یک برنامه ساده باشد یا reducer های متعددی وجود داشته باشد که از بخشها یا برشهایی از state سراسری در یک برنامه بزرگتر مراقبت میکنند.
به عنوان مثال، میتواند یک reducer باشد که state سبد خرید را در یک برنامه خرید مدیریت کند، سپس میتواند یک reducer وجود داشته باشد که قسمت جزئیات کاربر برنامه را مدیریت کند و غیره.
هر زمان که یک اکشن ارسال می شود، تمام reducer ها فعال می شوند. هر reducer عمل را با استفاده از دستور سوئیچ که نوع عمل را روشن می کند فیلتر می کند. هر زمان که دستور switch با اقدام انجام شده مطابقت داشته باشد، reducer های مربوطه اقدامات لازم را برای به روز رسانی و بازگرداندن یک نمونه جدید جدید از state سراسری انجام می دهند.
در ادامه مثال بالا، می توانیم یک کاهنده به صورت زیر داشته باشیم:
const initialCartState = {
noOfItemInCart: 0,
cart: []
}
// NOTE:
// It is important to pass an initial state as default to
// the state parameter to handle the case of calling
// the reducers for the first time when the
// state might be undefined
const cartReducer = (state = initialCartState, action) => {
switch (action.type) {
case "ADD_ITEM_TO_CART":
return {
...state,
noOfItemInCart: state.noOfItemInCart + 1,
cart : [
...state.cart,
action.payload
]
}
case "DELETE_ITEM_FROM_CART":
return {
// Remaining logic
}
default:
return state
} // Important to handle the default behaviour
} // either by returning the whole state as it is
// or by performing any required logic
در قطعه کد بالا، یک reducer به نام cartReducer
ایجاد کردیم که یک تابع JS خالص است. این تابع دو پارامتر state و عمل را می پذیرد.
توجه داشته باشید که پارامتر state یک پارامتر پیش فرض است که state اولیه را می پذیرد. این برای رسیدگی به سناریویی است که reducer برای اولین بار زمانی که مقدار state تعریف نشده است فراخوانی می شود.
همچنین توجه داشته باشید که هر reducer باید state پیشفرض را مدیریت کند که در آن، اگر هیچکدام از موارد سوئیچ با عمل انجام شده مطابقت نداشته باشد، reducer باید state را به همان شکلی که هست برگرداند یا هر منطق مورد نیاز را قبل از عبور از state روی آن انجام دهد.
هر زمان که اقدامی را با نوع خاصی ارسال میکنیم، باید مطمئن شویم که reducer های مناسب برای مدیریت آن عمل داریم.
در مثال بالا، با کلیک بر روی دکمه، یک اقدام با یک Action creator به نام ()addItemToCart
ارسال کردیم. این سازنده کنش اقدامی با نوع ADD_ITEM_TO_CART
ارسال کرده است.
بعد، ما یک reducer به نام cartReducer
ایجاد کرده ایم که state (با state اولیه پیش فرض) و عمل را به عنوان پارامتر می گیرد. نوع اقدام را روشن میکند، و سپس هر موردی که با نوع اقدام ارسال شده مطابقت داشته باشد، بهروزرسانی لازم را انجام میدهد و نسخه جدید و جدید state بهروزرسانی شده را برمیگرداند.
در اینجا توجه داشته باشید که state در redux غیر قابل تغییر است. بنابراین، reducer ها ابتدا یک کپی از کل state فعلی ایجاد میکنند، تغییرات لازم را ایجاد میکنند، و سپس یک نمونه جدید از state را با تمام تغییرات/بهروزرسانیهای لازم برمیگردانند.
بنابراین در مثال بالا، ابتدا یک کپی از کل state با استفاده از عملگر spread می آیم state...
ایجاد می کنیم. سپس noOfItemInCart
را 1 افزایش می دهیم، آرایه سبد خرید را با افزودن شی جدید ارسال شده در action.payload
نشان داده شده در زیر به روز می کنیم و در نهایت شی به روز شده را برمی گردانیم.
{
bookName: "Harry Potter and the Goblet of Fire",
noOfItem: 1,
}
پس از اینکه reducer ها state را به روز کردند، اگر ما برویم و state را console.log
کنیم، نتیجه زیر را خواهیم دید:
// Updated store
{
noOfItemInCart: 3, // Incremented by 1
cart: [
{
bookName: "Harry Potter and the Chamber of Secrets",
noOfItem: 1,
},
{
bookName: "Harry Potter and the Prisoner of Azkaban",
noOfItem: 1
},
{ // Newly added object
bookName: "Harry Potter and the Goblet of Fire",
noOfItem: 1,
}
]
}
خلاصه
به طور خلاصه، سه اصل زیر به طور کامل بر نحوه عملکرد Redux حاکم است:
state گلوبال یک برنامه کاربردی در یک درخت شی در یک Store ذخیره می شود.
تنها راه برای تغییر state، انتشار یک action است، که یک شی است که آنچه اتفاق افتاده را توصیف می کند.
برای مشخص کردن اینکه چگونه درخت state با اعمال تبدیل می شود، reducer های خام می نویسیم.
نتیجه
در این مقاله، ویژگیهای اصلی Redux و اینکه چگونه Redux میتواند برای برنامه شما مفید باشد، بحث کردیم. در حالی که Redux دارای ویژگی های مفید بسیاری است، این بدان معنا نیست که شما باید Redux را به همه برنامه های خود اضافه کنید. مهم است که بدانید چه زمانی از Redux استفاده نکنید.
یکی از مزایای اصلی Redux توانایی پیمایش در تاریخچه state است که به توسعه دهندگان این امکان را می دهد تا مشاهده کنند state در طول چرخه عمر برنامه چگونه تغییر کرده است. با این حال، اجرای Redux تنها در صورتی مهم است که با نیازهای شما مطابقت داشته باشد و پروژه شما به ابزار مدیریت stateنیاز داشته باشد.