توی مقاله " یادگیری redux، ساده تر از چیزی که فکرش رو میکنی! " ما درباره ریداکس صحبت کردیم و باهم یاد گرفتیم که چطور میتونیم ازش استفاده کنیم، اما همونطور که تو مقاله دیدیم و احتمالا موقع استفاده از ریداکس به ذهنتون خطور کرده اینه که کانفیگ کردن ریداکس پیچیده اس و از یه طرف خیلی تکرار واضحات نیاز داره و از طرف دیگه هم به خودی خود هیچ کار مفید و خاصی انجام نمیده و ما مجبوریم پکیجایی مثل redux-thunk, redux-persist , redux-saga و... نصب کنیم تا بتونیم ازش استفاده کنیم.
حالا redux-toolkit اومده که همه ی اینارو یکجا به ما بده و کار رو راحت تر کنه، ئس بهتره به این نکته دقت کنید که redux-toolkit در واقع یه راه برای ساده تر نوشتن ریداکسه و در پشت پرده داره دقیقا همون کارایی که با ریداکس میکردیم رو انجام میده منتها ما دیگه نیازی نیست با خیلی از این پیچیدگی هایی که ریداکس داشت سر و کله بزنیم.
ما قراره توی این مقاله یه استراکچر برای استفاده از متد های پر استفاده ریداکس تولکیت پیاده کنیم و بتونیم اکشن های async و sync بنویسیم و یه استراکچر کاملا متفاوت هم قراره داشته باشیم که توش قراره از rtk query استفاده کنیم که در واقع یه روش پیاده سازی ریداکس تولکیت هستش منتها این روش به ما توی فچ کردن و کش کردن دیتا هم کمک شایانی میکنه.
نکته: اگه اولین باره کلا میخواید ریداکس یاد بگیرید و استفاده کنید توصیه میکنم قبلش یه سر به مقاله ریداکس بزنید تا کلا با چراییِ استفاده ازش و نحوه استفاده ازش رو بدونید چون ما توی این مقاله قرار نیست بگیم چرا داریم از ریداکس استفاده میکنیم و برای چه کاری خوب هستش.
نکته: این مقاله رو بر این فرض نوشتم که شما هیچی از ریداکس نمیدونید و اولین باره میخواید وارد دنیای ریداکس شید و ترجیحتون ریداکس تولکیت هستش اما در عین حال یک سری نکات با عنوان "نکته ریداکسی" در طول مقاله گذاشتم که مخصوصِ کساییه که مقاله ریداکس رو خوندن و از قبل با ریداکس آشنا هستن، پس اگه با ریداکس هیچ آشنایی ندارید از خوندن نکات ریداکسی پرهیز کنید تا باعث گیج شدنتون نشه!
npm install @reduxjs/toolkit react-redux
این پکیج شامل همه ی چیزایی که ما نیاز داریم هستش از جمله redux-thunk , rtk query و...
از react-redux برای ارتباط گرفتن ری اکت و ریداکس استفاده میشه برای مثال تغییر دادن دیتای توی store و یا خوندن دیتای مربوطه از استیت گلوبال.
بعد از ساخت پوشه features، ما برای هر اکشنمون یه پوشه میسازیم. مثلا توی عکس بالا ما یه پوشه ساختیم برای رد و بدل کردن محصولاتمون در استیت گلوبال و یه پوشه برای کتگوری هامون و یه پوشه برای کارت (سبد خرید) و...
محتویات هر پوشه:
هر پوشه شامل دو فایل بالا خواهد بود و تنها تفاوتشون اسم فایل دوم خواهد بود که مثلا عکس بالا چون محتویات پوشه cart هستش پس اسم فایلِ slice ( فایل دومی) برابر با cartSlice هستش.
نکته ریداکسی: slice دقیقا مثل ردیوسر خواهد بود همونطور که توی ریداکس اسم فایل ردیوسر برابر با xxxReducer میبود اینجا هم به همین صورت xxxSlice خواهد بود و ما نیازی به فایل ردیوسر برای هر اکشن نخواهیم داشت
فایل action شامل تمام اکشن ها(فانکشن ها) یی خواهد بود که میخوایم ازشون استفاده کنیم تا دیتایی رو رد و بدل کنیم مثلا فایل action پوشه cart :
ما از createAsyncThunk برای ساخت اکشن های async استفاده میکنیم که اولین ورودی که میگیره در واقع یه استرینگ هستش که تایپ این اکشنمون به حساب میاد و مقدار دوم یه کال بک هستش که همون فانکشن async مون هستش که باید جاهای دیگه با اسمی که برای متغیر نوشتیم صداش کنیم برای مثال اولین اکشن عکس بالا getCart خواهد بود و با همین اسم اون کال بک رو صدا خواهیم زد و اگه نیازه که ارگیومنتی به این اکشنمون پاس بدیم در صورتی که فقط یدونه ارگیومنت نیاز داشت به صورت تکی مینویسیمش مثل اکشن deleteFromCart توی عکس بالا که فقط یدونه id رو نیاز داره ، اما اگه اکشنمون به چند تا ارگیومنت نیاز داره باید به صورت ابجکت بگیریمش مثل اکشن addToCart یا updateCart توی عکس بالا.
حالا ما داخل این کال بک بصورت async/await ریکوئستمون رو میزنیم( حالا چه با axios چه با fetch چه با هر چیزی) و دیتایی که میگیریم رو ریترن میکنیم در واقع چیزی که این کال بک قراره برای slice به طور خودکار بفرسته شامل یه ابجکت خواهد بود که پراپرتی به اسم payload خواهد داشت که همون دیتاییه که داریم از سرور میگیریم و ریترن میکنیم
نکته ریداکسی: همونطور که متوجه شدید ما با متد createAsyncThunk نیازی به استفاده از redux thunk نخواهیم داشت و ریداکس خودش پشت پرده هندل میکنه قضیه رو و همینطور مقدار اولی که createAsyncThunk گرفت رو همون type ای در نظر بگیرید که توی ریداکس با اکشن هامون میفرستادیم و باید هر تایپ منحصر به فرد میبود.
خب حالا بعد از نوشتن اکشن هامون نیاز به یک slice خواهیم داشت تا روی این اکشن ها نظارت کنه و یه جورایی بعد از صدا شدن اکشن هامون مقدار ریترن شده رو( payload) رو بگیره و بفرسته تو استیت گلوبال.
پس یه فایل cartSlice برای مثال بالا میسازیم که محتواش به این صورت خواهد بود:
خب ما یه متد createSlice خواهیم داشت که یه آبجکت میگیره و توش میتونیم آپشن های به خصوصی رو بنویسیم از جمله name که اسم slice ما خواهد بود
نکته : اگر دقت کنید میتونید متوجه این قضیه بشید که اسمی که به slice مون میدیم همون اسمی هستش که ازش توی پارامتر اول createAsyncThunk قبل از اسلش " / " استفاده کردیم ولی بعد از "/" ، هر اکشن، تایپ به خصوص خودش رو خواهد داشت ما اینطوری داریم میگیم که slice ای به نام cart به این اکشن ها نظارت خواهد کرد و در کل خوبی این کار اینه که وقتی یکی از اکشن هارو صدا بزنیم اون slice ای که داره بهش نظارت میکنه قادر خواهد بود زمان pending و rejected و fulfilled اون اکشن رو داشته باشه و ازش بتونیم استفاده های مفیدی کنیم
دومین آپشن، initialState خواهد بود که برابر با مقدار اولیه برای استیتمون هستش که توی عکس بالا یه آبجکت با پراپرتی value که به صورت دیفالت nullهستش داریم.
نکته ریداکسی: دقیقا مثل توی reducer که ما یه مقدار initialState داشتیم اینجا هم به همون نیاز داریم
سومین آپشن، extraReducers خواهد بود که برابر با یه فانکشن هستش که یک پارامتر به اسم builder میگیره و بیلدر یه متدب ه اسم addCase داره که ما ازش برای آپدیت کردن استیتمون در شرایط مختلف استفاده خواهیم کرد
متد addCase دوتا پارامتر میگیره اولی وضعیتی هست که میخوایم و دومی یه کال بک هست که توش میایم وابسته به اون وضعیت یه بلایی سر initialState میاریم و این کال بک دوتا پارامتر میگیره اولی همون state اولیه ای هست که میخوایم اپدیتش کنیم و دومی action هستش که از اکشنمون ریترن شده و یه پراپرتی payloadداره که مساوی با دیتایی هستش که از سرور گرفته: مثلا توی مثال بالا ما هر چهار تا اکشن هامون رو ایمپورت کردیم و برای هر کدوم یه addCase نوشتیم توی اولین addCase گفتیم اگه اکشن getCart مخابره شد و وضعیت fulfilled داشت بیا پراپرتی value داخل استیت رو به دیتایی که داره از این اکشن برمیگرده تغییر بده و همینطور پشت سرش دوباره سه تا .addCase دیگه نوشتیم برای اکشن های دیگه مون
اینم یه مثال دیگه از یه slice متفاوت برای یه اکشن دیگه که توش وضعیت های جذاب تری نوشتیم:
توی مثال بالا ما یه پراپرتی error و loading هم داریم که میتونیم توی شرایطی که وضعیت اکشنمون pending یا rejected بود ازش استفاده کنیم تا برای مثال توی یو آی ازشون برای لودینگ یا نشون دادن ارور استفاده کنیم تا حس کاربری بهتری رو ایجاد کنیم
نکته: توی ریداکس معمولی معمولا باید یه استیت گلوبال لودینگ یا یا استیت لیودینگ داخلی میداشتیم تا ازش برای نشون دادن لودینگ استفاده کنیم به صورتی که قبل از ریکوئست، لودینگ رو با مقدار true مخابره میکردیم و بعد از پایان ریکوئست همون لودینگ رو با مقدارfalse مخابره میکردیم، اما با ریداکس تولکیت کارمون خیلی راحت تر شده و نیازی به استیت اضافه ای مثل لودینگ و ارور نیست
نکته مهم: آبجکت ها توی جاوااسکریپت immutable یا تغییر ناپذیر هستن و نمیتونیم همینطوری یه پراپرتیش رو برابر با یه دیتایی قرار بدیم و میومدیم بجاش به اینصورت استیت رو اپدیت میکردیم:
return {...initialState , value: payload.action}
به این صورت ما کل ابجکت قبلی رو کپی میکردیم و اون پراپرتی که میخواستیم تغییر بدیم رو برابر با مقدار جدید میذاشتیم( و عملا یه ابجکت جدید رو جاگذاری میکردیم به جای قبلی).
این روش ممکنه یه مقدار رو مخ باشه و از همین جهت توی redux toolkit ما نیازی به رعایت این قانون نداریم و میتونیم خیلی راحت هر مقداری رو مساوی با مقدار جدید بزاریم که البته ما فقط توهم این رو خواهیم داشت که داریم اون ابجکتمون رو تغییر میدیم اما در پشت پرده ریداکس تولکیت از پکیجی به اسم immer استفاده میکنه تا بیاد چیزی که ما نوشتیم رو به همون روش کپی کردن قبلی بنویسه
نکته: اگه توی وضعیت pending لودینگ رو مساوی با true یا هر مقداری قرار دادیم باید توی شرایط دیگه در صورتی که دیگه بهش نیاز نداریم مقدارشو به همون مقدار دیفالت برگردونیم مثل توی عکس بالا که توی شرایط ارور یا fulfilled، لودینگ رو false کردیم چون نباید true میموند و بهش نیازی نداشتیم.
چهارمین آپشنمون، reducers خواهد بود که ازش برای نوشتن اکشن های sync استفاده میشه و حتی نیازی نیست توی یه فایل جدا اکشنارو بنویسیم، اپشن reducers در واقع یه ابجکته که توش فانکشن هامون رو مینویسیم که همون اکشن های sync ما هستن و مقدار اولی که میگیرن استیت هست که همون initialState خودمونه که میخوایم تغییرش بدیم و مقدار دوم که میگیرن action هستش که یه payload داره که همون دیتایی میشه که ما موقع صدا کردن این اکشنمون بهش به عنوان پارامتر اول پاس میدیم مثال:
وقتی اکشنی داخل reducers مینویسیم متد createSlice اون اکشن هارو برا ما برمیگردونه و طبق عکس بالا باید از پراپرتی modeSLice.actions به صورت دی استراکچر شده اکشن هارو بگیریم و اکسپورت کنیم تا بتونیم ازشون برای رد و بدل کردن دیتا استفاده کنیم.
اگه دقت کرده باشید توی خط اخر فایل های xxxSlice ، ما یه reducer داریم از slice برمیگردونیم که در واقع به ما اجازه میده به صورت reducer توی استیت گلوبال اضافش کنیم
متغیر store همون استیت گلوبال ما خواهد بود که در نهایت هر اکشنی یه دیتایی رو مخابره کنه توی پراپرتی های همین استیت ذخیره میشند.
برای ساخت این استیت گلوبال نیاز به configureStore داریم که یه ابجکت میگیره که توش میتونیم آپشن هایی بنویسیم از جمله reducer که یه ابجکت از ردیوسر هایی خواهد بود که توسط createSlice ساخته و اکسپورت میشن
نکته ریداکسی : با متد configureStore دیگه نیازی نیست از combineReducers و یا حتی کانفیگ reduxDevTool استفاده کنیم خود این متد اینارو اضافه میکنه.
بعد از ایمپورت کردن store، با provider دور کامپوننت App که هفت جد و آباد همه ی کامپوننتای دیگس محاصره میکنیم و بهش پراپی میدیم که store رو دربر داره و حالا هر کامپوننتی داخل app.js هر چقدر هم تو در تو باشه میتونه به صورت مستقیم به هر دیتایی دسترسی داشته باشه بدون اینکه از بالا تا پایین یکی یکی فلان دیتا رو بهش پاس بدیم که در ادامه میگم چطور.
ما یا میخوایم یه اکشن رو بفرستیم تا دیتا رو تغییر بده و یا میخوایم دیتایی که تغییر کرده رو از ردیوسر بگیریم و توی یو ای نمایش بدیم
. مخابره یا ارسال کردن دیتا برای تغییر استیت(dispatch):
اگه اکشن addToCart رو توی مثالای بالا یادتون باشه ما اینجا اومدیم ایمپورتش کردیم و همینطور یه هوک به اسم useDispatch از react redux ایمپورت کردیم که به شکل بالا میتونیم ازش استفاده کنیم تا اکشنی که داشتیم رو مخابره کنیم و نکته ی دیگه اینکه اکشنمون دوتا پارامتر id و quantityNumber میگرفت که باید توی ابجکت میذاشتیمش پس اینجا دوتا پارامتر رو داخل ابجکت به اینصورت که میبینید پاس میدیم.
. خوندن دیتا از استیت گلوبالی که داریم:
ما هوکی به اسم useSelector رو از rect-redux ایمپورت میکنیم و این هوک یه کال بک میگیره که این کال بک یه پارامتر استیت داره که همون آبجکت استیت گلوبالِ ما توی store هستش و میتونیم از پراپرتی های مختلفی که هر کدوم برابر با یکی از ردیوسر ها بودن استفاده کنیم و توی مثال بالا ما داریم ازدیتای cart توی هدر استفاده میکنیم که یه پراپرتی value داشت پس ما هر سری که اون addToCart رو مخابره کنیم اون slice میاد و در صورت fulfilled بودن (همونطور که نوشتیم) مقدار value ی داخل cart رو با دیتایی که از اکشنمون گرفته اپدیت میکنه. اگه کارت علاوه بر value پراپرتی های دیگه ای مثل error , loading داشت هم میتونستیم ازشون به صورت شرط استفاده کنیم و یو آی بهتری داشته باشیم.
اگه به دنبال استراکچر متفاوتی هستید که توی data fetching و data caching (کش کردن دیتا و ممانعت از ریکوئست های غیر ضروری) هم بهتون کمک کنه این قسمت مخصوص شماس.
اولین کاری که باید بکنید اینه که بیخیال استراکچری که تا الان دربارش حرف زدیم بشید و به صورت زیر کانفیگ ریداکس رو انجام بدید.
پوشه services شامل یه فایل هستش به اسم xxxApi شما میتونید هر چیزی به جای xxx بزارید ( توی مثال ما اسمشو میزاریم onlineShopApi)
محتوای این فایل به این صورته:
ما با createApi میتونیم از اندپوینت هایی که داریم برای ریکوئست های مختلف استفاده کنیم و یه data fetching تر تمیز در بیاریم ازش. متد createApi یه ابجکت از آپشن ها میگیره که شامل مهمتریناش که توی عکس هم میبینید شامل موارد زیر هستش:
آپشن reducerPath : این آپشن در واقع یه کلید برای سرویس api شما خواهد بود که ازش توی استیت گلوبال جلوتر قراره استفاده کنیم و این اسم باید منحصر به فرد باشه.
آپشن baseQuery : مقدارش مساوی با fetchBaseQuery هستش که از ریداکس تولکیت ایمپورتش میکنیم، fetchBaseQuery یه wrapper دورِ fetch هستش که صرفا ریکوئست هارو ساده تر میکنه و یه آبجکت به عنوان ورودی میگیره که میتونیم توش نیازداریم رو قرار بدیم مثلا ما توی ریکوئست هامون یه baseUrl داریم که ثابته و یه سری اندپوینت داریم که متنوعه و آپشن baseUrl برای مشخص کردنِ baseUrl هستش.
آپشن endpoints : مقدارش مساوی با یه فانکشن هستش که یه پارامتر build میگیره و این فانکشن یه ابجکت برمیگردونه که شامل همه ی ریکوئست های ما با اندپوینت های مختلف هستش هر ریکوئست مساوی با builder.query یا builder.mutation هست که یه فانکشن هستن و یه ابجکت میگیرن:
اگه ریکوئستمون متد get هستش با builder.query مینویسیمش که داخل ابجکتی که میگیره یه فانکشن query قرار میدیم که اندپوینتِ اون ریکوئستمون رو برمیگردونه(مثل getProductDetails یا getCart توی مثال بالا) اگه نیاز به ایدی یا دیتا توی ریکوئستمون داشته باشیم باید به عنوان پارامتر query دریافتش کنیم.
اگه ریکوئستمون متد هایی برای آپدیت کردن دیتا هستند مثل post , patch , put , delete ، با builder.mutation مینویسیمش که داخل ابجکتی که میگیره یه فانکشن query قرار میدیم که url ( اندپوینت ) و method و در صورت نیاز body رو برمیگردونه ( مثل addToCart و deleteFromCart توی مثال بالا) طبق معمولاگه به ایدی یا دیتای body نیاز داشتیم میتونیم به صورت پارامتر query دریافتش کنیم.
حالا createApi با توجه به این آپشن هایی که دادیم برای هر اندپوینتمون یه هوک میسازه و برمیگردونه و ما میتونیم با دی استراکچر کردن بگیریمشون و اکسپورتشون کنیم تا هر جا که نیاز بود با این هوک ها ریکوئست هامون رو بزنیم و این هوک ها به این شکل اسم گذاری میشن(مثل توی مثال بالا خط آخر):
برای builder.query ها :
use + اسم اندپوینت شما + Query = مثلا useGetCartQuery()
برای builder.mutation ها:
use + اسم اندپوینت شما + Mutation = مثلا useDeleteFromCartMutation()
علاوه بر این هوک ها، createApi برامون یه ردیوسر هم میده که ازش توی استیت گلوبالمون استفاده کنیم که توی قدم دوم قراره اینکارو کنیم.
همونطور که حدس میزنید ما فایل store رو میزنیم تا استیت گلوبالمون رو توش کانفیگ کنیم در صورتی که از استراکچر rtk query استفاده کنیم باید محتوی این فایل به این صورت باشه:
میبینیم که برای اسم ردیوسری که از api میگیریم از آپشن reducerPath استفاده میکنیم و اینجا بدردمون خورد.
تفاوت چشمگیری که این کانفیگ با کانفیگ قبلی داشت اینه که ما اینجا جز ردیوسر یه اپشن دیگه هم به configureStore اضافه کردیم به اسم middleware. به صورت دیفالت وقتی این آپشن رو اضافه نکنیم یه سری middleware به استور اضافه میشن و ما با قرار دادن این اپشن میتونیم بگیم که چه middleware هایی اضافه شن و در نتیجه باید ارایه ای از middleware ها داشته باشیم برای این اپشن. توی عکس بالا ما با کمک getDefaultMiddleware گفتیم که بیا همه ی middleware های دیفالتی که همیشه اضافه میکنی رو به اضافه middleware هایی که از onlineShopApi میان رو با هم به استور اضافه کن
برای مثال یکی از middleware های دیفالتی که همیشه ادد میشه thunk هستش که باعث ارتباط گرفتن ریداکس با اکشن های async میشه
نکته ریداکسی: هر middleware ای که توی لیست middleware ها قرار میدیم در واقع قراره توسط applyMiddleware که توی ریداکس هم داشتیم به استور اضافه بشن.
نکته: middleware هایی که از api میگیریم و به middleware های دیفالت اضافه میکنیم در واقع در جهت کمک به کش کردن دیتا و ولیدیشن و... ازشون استفاده میشه.
این مرحله دقیقا مثل مرحله 4 روش قبلی هستش و هیچ تفاوتی نداره.
ما یا میخوایم به وسیله ی هوک های query ریکوئست بزنیم و دیتایی رو get کنیم و یا میخوایم به وسیله ی هوک های mutation ریکوئست بزنیم و دیتایی رو بفرستیم.
. گرفتن دیتا به وسیله ی هوک های query :
طبق مثال بالا، هوک های query به ما پراپرتی هایی مثل isLoading و data و isError رو برمیگردونن و ما دیگه نیازی نداریم توی استیت گلوبال با چیزایی مثل loading و error نوشتن سر و کله بزنیم به محض mount شدن کامپوننت ریکوئست زده میشه و دیتا رو میگیره و از دفعات بعد اون دیتا cache میشه و دیگه لودینگ نخواهیم داشت ( لودینگ فقط اولین بار که دیتا خالیه و کش نشده نشون داده میشه) و بجاش دیتای از قبل کش شده رو نشون میده.
. پست کردن، ادیت کردن، حذف کردن دیتا به وسیله ی هوک های mutation:
طبق مثال بالا، هوک های mutation یه فانکشن بهمون برمیگردونن که میتونیم صدا بکنیمش و پارامتر هایی که میخواست رو به صورت ابجکت بهش پاس بدیم( اگه یه پارامتر میخواد میتونین داخل ابجکت نزارینش) و این هوک علاوه بر فانکشن، یه ابجکت هم برمیگردونه که نتایج ریکوئستمون رو برامون میفرسته این ابجکت نتایج شامل پراپرتی هایی از جمله isLoading و isError و data و... هستش که ازشون میتونیم توی یو آی استفاده کنیم.
این دومین روشی بود که میشه از ریداکس استفاده کرد و چیزی که باید بهش توجه کرد اینه که چه توی روش اول چه توی روش دوم برای هر متد کلی آپشن و کلی راه مختلف پیاده سازی و کلی متد و تکنیک دیگه وجود داره که مسلما نمیشه همشون رو توی مقاله گنجوند.
توی این مقاله سعی کردم که ساده ترین روش پیاده سازی رو با چند تا از مهم ترین و پر کاربردترین متد هارو براتون توضیح بدم تا برای شروع بتونید ازش استفاده بکنید اما اگه خیلی به ریداکس تولکیت علاقه مند شدید میتونید به داکیومنت رسمیش سر بزنید و دانشتون رو در این زمینه با یادگیری مابقی متد ها و آپشن ها کامل کنید.
خدانگهدار و موفق باشی ?