در برنامههای پیچیده React، مدیریت state برنامه به طور موثر میتواند به یک چالش بزرگ تبدیل شود. اینجا جایی است که Redux، که یک کتابخانه مدیریت state قابل پیشبینی میباشد، وارد عمل میشود. Redux با معرفی یک جریان داده یک طرفه، نظم و وضوح در نحوه بهروزرسانی دادهها و تعامل در کامپوننتهای React را به ارمغان میآورد. در این مقاله قصد داریم تا به بررسی عملکرد Redux بپردازد، و به طور خاص بر روی نحوه جریان داده در برنامه تمرکز کنیم. ما مفاهیم اصلی مانند Redux store، اکشنها، reducerها و selectorها را به همراه مثالهای عملی از نحوه کارکرد آنها برای مدیریت یکپارچه state برنامه باهم بررسی خواهیم کرد.
Redux یک state container قابل پیشبینی برای برنامههای جاوااسکریپت است که در درجه اول با کتابخانههایی مانند React مورد استفاده قرار میگیرد. این container به مدیریت state برنامه در یک store متمرکز کمک میکند و مدیریت و بهروزرسانی state را در کل برنامه آسانتر مینماید.
به زبان ساده، Redux راهی برای ذخیرهسازی و مدیریت دادههایی که برنامه ما برای کار به آنها نیاز دارد، ارائه میدهد. همچنین از یک الگوی دقیق پیروی میکند تا اطمینان حاصل شود که تغییرات state قابل پیشبینی و مدیریت هستند.
استفاده از Redux برای مدیریت داده در برنامههای خود چندین مزیت دارد:
درک مفاهیم اصلی جریان داده در Redux برای تسلط بر مدیریت state در برنامههای جاوااسکریپت مدرن ضروری است.
Redux از یک الگوی جریان داده دقیق یک طرفه پیروی میکند. به این معنی که دادهها در برنامه ما از طریق یک سری مراحل در یک جهت حرکت میکنند:
getState()
، بهروزرسانی state از طریق dispatch(action)
و ثبت listenerها از طریق subscribe(listener)
را فراهم میکند.در ادامه یک نمای کلی ساده از نحوه عملکرد جریان داده یک طرفه در Redux را داریم:
store.dispatch(action)
اکشنها را به Redux store ارسال میکنند. اکشنها آبجکتهای جاوااسکریپت ساده با یک فیلد type
هستند که نوع عمل انجام شده را توصیف میکند.مدیریت state نقش بسیار مهم و اساسی در توسعه وب مدرن ایفا میکند و تضمین مینماید که اپلیکیشنها، stateهای سازگار و قابل پیشبینی را در بین کامپوننتهای مختلف حفظ میکنند.
Store مهمترین قسمت مدیریت state با Redux است و کل درخت state برنامه را نگه میدارد. store به ما اجازه میدهد تا:
store.getState()
به state فعلی برنامه خود دسترسی پیدا کنیم.store.dispatch(action)
ارسال نماییم.store.subscribe(listener)
متناسب با آن تغییرات، بهروزرسانی شوند.در اصل، Redux Store به عنوان یک مخزن متمرکز برای state برنامه ما عمل میکند، جریان داده قابل پیشبینی را تسهیل کرده و مدیریت state را آسانتر میکند.
state موجود در Redux بیانگر state کل برنامه ما میباشد. معمولاً به عنوان یک آبجکت ساده جاوااسکریپت ساختار یافته است. شکل state توسط reducerها تعریف میشود. به عنوان مثال:
const initialState = { todos: [], visibilityFilter: 'SHOW_ALL', };
در این مثال، todos
و visibilityFilter
بخشهایی از state هستند که توسط Redux مدیریت میشوند.
Reducerها توابعی هستند که نحوه تغییر state برنامه را در پاسخ به اکشنهای ارسال شده به store مشخص میکنند. آنها state فعلی و یک اکشن را به عنوان آرگومان دریافت میکنند و بر اساس نوع اکشن، state جدید را return میکنند.
const todosReducer = (state = [], action) => { switch (action.type) { case 'ADD_TODO': return [ ...state, { id: action.id, text: action.text, completed: false } ]; case 'TOGGLE_TODO': return state.map(todo => (todo.id === action.id) ? { ...todo, completed: !todo.completed } : todo ); default: return state; } };
در این مثال، todosReducer
قسمتی از state todos
را مدیریت میکند و اکشنهایی مانند 'ADD_TODO'
و 'TOGGLE_TODO'
را برای اضافه کردن todoهای جدید یا تغییر وضعیت تکمیل آنها بررسی مینماید.
Actionها آبجکتهای ساده جاوااسکریپت هستند و آنچه را که در برنامه اتفاق افتاده است را توصیف میکنند. آنها تنها منبع اطلاعاتی برای store هستند. اکشنها معمولاً یک فیلد type
دارند که نوع اکشن انجام شده را نشان میدهد، و همچنین ممکن است دادههای اضافی لازم برای اکشن را نیز با خود حمل نمایند.
const addTodo = (text) => ({ type: 'ADD_TODO', id: nextTodoId++, text }); const toggleTodo = (id) => ({ type: 'TOGGLE_TODO', id });
در این مثال، addTodo
و toggleTodo
توابع ایجاد اکشن هستند که به ترتیب اقداماتی را برای افزودن یک تسک جدید و تغییر وضعیت تکمیل یک تسک انجام میدهند.
رابطه بین این المنتها در Redux برای مدیریت موثر state برنامه بسیار مهم است:
این کامپوننتها با هم ساختار اصلی مدیریت state در Redux را تشکیل میدهند و راهی واضح و قابل پیشبینی برای مدیریت و بهروزرسانی state برنامه در کل برنامه ارائه میدهند.
مدیریت state به طور موثر در هسته ایجاد اپلیکیشنهای پویا و ریسپانسیو نهفته است. اکشنها، در معماری Redux و کتابخانههای مدیریت state مشابه، به عنوان المنتهای مهم برای شروع تغییرات state عمل میکنند.
Action creatorها در Redux توابعی هستند که آبجکتهای اکشن را ایجاد و return میکنند. این action objectها آنچه را که در برنامه ما اتفاق افتاده است را توصیف میکنند و به Redux store فرستاده میشوند تا تغییرات state را آغاز نمایند.
درنهایت، action creatorها منطق ایجاد اکشنها را کپسوله میکنند و کد ما را ماژولارتر کرده و تست آن را آسانتر مینمایند.
در ادامه یک نمونه از یک action creator را داریم:
// Action creator function const addTodo = (text) => ({ type: 'ADD_TODO', id: nextTodoId++, text }); // Usage of the action creator const newTodoAction = addTodo('Buy groceries');
باتوجه به مثال بالا:
addTodo
یک تابع action creator است که text
را به عنوان پارامتر میگیرد و یک action object را return میکند.type
('ADD_TODO'
) است که نوع اکشن و فیلدهای اضافی (id
و text
) را مشخص میکند که دادههای لازم را برای اکشن فراهم مینماید.به طور کلی، action creatorها فرآیند ایجاد اکشنها را ساده میکنند. بهویژه زمانی که اکشنها قبل از ارسال، به دادهها یا محاسبات پیچیده نیاز دارند.
Action typeها در Redux ثابتهای رشتهای هستند که نوع اکشن انجام شده را مشخص میکنند. آنها برای شناسایی و تمایز اکشنهای مختلفی که میتوانند به Redux store ارسال شوند، مورد استفاده قرار میگیرند. Redux با استفاده از ثابتهای رشتهای برای action typeها، تضمین میکند که آنها منحصربهفرد هستند و در سراسر برنامه به آسانی قابل ارجاع میباشند.
در ادامه نحوه تعریف action type را داریم:
// Action types as constants const ADD_TODO = 'ADD_TODO'; const TOGGLE_TODO = 'TOGGLE_TODO'; const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER';
این ثابتها (ADD_TODO
، TOGGLE_TODO
، SET_VISIBILITY_FILTER
) اکشنهای مختلفی را نشان میدهند که میتوانند در برنامه ما اتفاق بیفتند، مانند افزودن یک تسک، تغییر وضعیت تکمیل یک تسک یا تنظیم یک فیلتر برای تسکها.
Action typeها معمولاً در action objectهای ایجاد شده توسط action creatorها مورد استفاده قرار میگیرند و در reducerها با هم تطبیق داده میشوند تا مشخص شود state چگونه باید در پاسخ به هر اکشن تغییر نماید.
// Example of using action types in a reducer const todosReducer = (state = [], action) => { switch (action.type) { case ADD_TODO: return [ ...state, { id: action.id, text: action.text, completed: false } ]; case TOGGLE_TODO: return state.map(todo => (todo.id === action.id) ? { ...todo, completed: !todo.completed } : todo ); default: return state; } };
مطابق آنچه که در مثال بالا دیدیم:
ADD_TODO
و TOGGLE_TODO
action typeهایی هستند که در todosReducer
برای مدیریت انواع مختلف اکشنها استفاده میشوند ('ADD_TODO'
و 'TOGGLE_TODO'
).action.type
در دستور switch تضمین میکند که reducer به هر اکشن ارسال شده، بر اساس نوع آن پاسخ مناسب میدهد.در قلب مدیریت state، مفهوم reducerها طراحی شده است تا انتقال state را به شیوهای کنترلشده و تغییرناپذیر مدیریت کند.
Reducerها در Redux توابع pure هستند که مسئول تعیین نحوه تغییر state برنامه در پاسخ به اکشنهای ارسال شده به store میباشند. آنها state فعلی و یک اکشن را به عنوان آرگومان دریافت میکنند و بر اساس نوع اکشن، state جدید را return میکنند.
در ادامه خلاصهای از نحوه کار reducerها و نقش آنها در مدیریت تغییرات state را باهم بررسی میکنیم:
Reducerها توابع pure هستند، به این معنی که آنها:
مدیریت State Transitionها: Reducerها مشخص میکنند که چگونه state برنامه در پاسخ به انواع مختلف اکشنها تغییر میکند. آنها از state فعلی و اکشن ارسال شده برای محاسبه و return کردن state جدید استفاده مینمایند.
// Example of a todos reducer const todosReducer = (state = [], action) => { switch (action.type) { case 'ADD_TODO': return [ ...state, { id: action.id, text: action.text, completed: false } ]; case 'TOGGLE_TODO': return state.map(todo => (todo.id === action.id) ? { ...todo, completed: !todo.completed } : todo ); default: return state; } };
با توجه به مثال بالا:
todosReducer
یک تابع pure است که state
(آرایه todos فعلی) و action
را به عنوان آرگومان میگیرد.action.type
، state جدیدی را محاسبه و return میکند (آرایه todos بهروزرسانی شده).بهروزرسانی تغییرناپذیر state: توجه به این نکته لازم است که reducerها هرگز نباید مستقیماً state را تغییر دهند. در عوض، کپیهایی از state ایجاد میکنند و آنها را برای تولید یک آبجکت جدید تغییر میدهند. این موضوع تضمین میکند که Redux میتواند تغییرات state را تشخیص دهد و کامپوننتها را بهطور مؤثر بهروزرسانی نماید.
اصل مسئولیت منفرد: هر reducer معمولاً بهروزرسانیهای یک بخش خاص از state برنامه را مدیریت میکند. این موضوع به تفکیک واضح نگرانیها کمک میکند و درک، تست و نگهداری reducerها را آسانتر مینماید.
توابع pure، از جمله reducerهای Redux، دارای ویژگیهای خاصی هستند که آنها را برای مدیریت تغییرات state مناسب میکند:
قطعی بودن: یک تابع pure همیشه خروجی یکسانی را برای ورودی یکسان تولید میکند. این پیشبینیپذیری تضمین میکند که reducerها بهطور مداوم این گونه رفتار میکنند، در نتیجه استدلال کردن در مورد آن آسانتر است.
بدون Side Effect بودن: توابع pure آرگومانهای ورودی یا هیچ state خارجی را تغییر نمیدهند. آنها فقط به پارامترهای ورودی خود وابستگی دارند و بدون ایجاد side effect قابل مشاهده، خروجی تولید میکنند.
دادههای غیرقابل تغییر: توابع pure دادهها را تغییر نمیدهند. در عوض، آنها ساختارهای دادهای جدیدی را ایجاد و return میکنند. در Redux، reducerها یک آبجکت state جدید را بدون تغییر state موجود، تولید میکنند که تشخیص کارآمد تغییرات و مدیریت state را ممکن میسازد.
شفافیت ارجاعی: میتوانیم توابع pure را بدون اینکه بر صحت برنامهای که داریم تأثیر بگذارد با مقادیر بازگشتی خود جایگزین کنیم. این ویژگی از ترکیبپذیری پشتیبانی کرده و تست و استدلال در مورد کد را آسانتر میکند.
تابع reducer، در هسته خود، نحوه تغییر state برنامه در پاسخ به اکشنهای ارسال شده را تعریف میکند. این تابع دو پارامتر میگیرد: state فعلی و یک action object که state جدید را بر اساس نوع اکشن دریافت شده تعیین مینماید.
یک تابع reducer در Redux یک تابع pure است که دو پارامتر را میگیرد: state قبلی (state قبل از اعمال اکشن) و یک action object. این پارامترها نحوه محاسبه state بعدی برنامه را توسط reducer تعیین میکنند.
state قبلی: این پارامتر نشان دهنده state فعلی برنامه قبل از ارسال اکشن است. تغییرناپذیر بوده و نباید مستقیماً در reducer اصلاح شود.
Action Object: یک action object یک آبجکت ساده جاوااسکریپت است و آنچه را که در برنامه اتفاق افتاده است را توصیف میکند. معمولاً دارای یک فیلد type
است که نوع اکشن انجام شده را نشان میدهد. سایر فیلدها در action object ممکن است دادههای اضافی لازم برای بهروزرسانی state را ارائه دهند.
const action = { type: 'ADD_TODO', id: 1, text: 'Buy groceries' };
در این مثال، action.type
که داریم 'ADD_TODO'
است، که نشان میدهد ما میخواهیم یک آیتم todo جدید به state اضافه کنیم.
تابع reducer باید state بهروزرسانی شده را بر اساس state قبلی و action object به آن ارسال کند. state بهروزرسانی شده معمولاً یک آبجکت جدید است که state برنامه را پس از اعمال اکشن نشان میدهد.
در ادامه ساختار اصلی یک تابع reducer را داریم:
const initialState = { todos: [], visibilityFilter: 'SHOW_ALL' }; const todoAppReducer = (state = initialState, action) => { switch (action.type) { case 'ADD_TODO': return { ...state, todos: [ ...state.todos, { id: action.id, text: action.text, completed: false } ] }; case 'TOGGLE_TODO': return { ...state, todos: state.todos.map(todo => (todo.id === action.id) ? { ...todo, completed: !todo.completed } : todo ) }; case 'SET_VISIBILITY_FILTER': return { ...state, visibilityFilter: action.filter }; default: return state; } };
بر اساس مثال بالا:
todoAppReducer
یک تابع reducer است که state تسکها و فیلترهای visibility را مدیریت میکند.state
(state قبل) و action
را به عنوان پارامتر میگیرد.action.type
، یک state object جدید را محاسبه و return میکند که تغییرات ایجاد شده توسط اکشن را منعکس مینماید.بهروزرسانی غیرقابل تغییر: reducerها هرگز نباید مستقیماً state قبلی را تغییر دهند. در عوض، با کپی کردن state قبلی (...state
) و اعمال تغییرات در آن، یک آبجکت جدید ایجاد میکنند.
حالت پیشفرض: اگر reducer نوع اکشن را تشخیص ندهد، case default
در دستور switch
state فعلی را بدون تغییر return میکند. این موضوع تضمین میکند که reducer همیشه یک state object معتبر را return میکند، حتی اگر هیچ تغییری ایجاد نشود.
مسئولیت منفرد: هر case در دستور switch
مربوط به یک نوع اکشن خاص است و مسئول بهروزرسانی یک بخش خاص از state برنامه میباشد. این امر باعث تفکیک واضح نگرانیها شده و درک و نگهداری reducerها را آسانتر میکند.
ما در Redux میتوانیم با استفاده از دستورهای switch یا منطق شرطی، اکشنهای مختلفی را در reducerها انجام دهیم. هدف هر دو رویکرد تعیین چگونگی تغییر state برنامه بر اساس نوع اکشن ارسال شده میباشد.
دستورات switch معمولاً در reducerهای Redux برای مدیریت انواع اکشنهای مختلف مورد استفاده قرار میگیرند. هر case
در دستور switch مربوط به یک نوع اکشن خاص است و reducer، منطق مربوطه را بر اساس نوع اکشن اجرا میکند.
در ادامه مثالی از روش استفاده از دستورات switch در یک reducer را باهم بررسی میکنیم:
const initialState = { todos: [], visibilityFilter: 'SHOW_ALL' }; const todoAppReducer = (state = initialState, action) => { switch (action.type) { case 'ADD_TODO': return { ...state, todos: [ ...state.todos, { id: action.id, text: action.text, completed: false } ] }; case 'TOGGLE_TODO': return { ...state, todos: state.todos.map(todo => (todo.id === action.id) ? { ...todo, completed: !todo.completed } : todo ) }; case 'SET_VISIBILITY_FILTER': return { ...state, visibilityFilter: action.filter }; default: return state; } };
با توجه این مثال:
todoAppReducer
از یک دستور switch برای مدیریت انواع اکشنهای مختلف استفاده میکند ('ADD_TODO'
،'TOGGLE_TODO'
،'SET_VISIBILITY_FILTER'
).case
مشخص میکند که state چگونه باید در پاسخ به نوع اکشن مربوطه بهروزرسانی شود.default
state فعلی را بدون تغییر return میکند و تضمین مینماید که reducer همیشه یک state object معتبر را برمیگرداند.از طرف دیگر، reducerها همچنین میتوانند از منطق شرطی (عبارات if-else
) برای تعیین نحوه بهروزرسانی state بر اساس نوع اکشن استفاده کنند. در حالی که استفاده از این روش نسبت به دستورات switch در Redux از رواج کمتری برخوردار است، اما منطق شرطی هم میتواند به طور مشابه برای مدیریت اکشنها استفاده شود.
مثالی از استفاده از منطق شرطی در یک reducer را در ادامه داریم:
const todoAppReducer = (state = initialState, action) => { if (action.type === 'ADD_TODO') { return { ...state, todos: [ ...state.todos, { id: action.id, text: action.text, completed: false } ] }; } else if (action.type === 'TOGGLE_TODO') { return { ...state, todos: state.todos.map(todo => (todo.id === action.id) ? { ...todo, completed: !todo.completed } : todo ) }; } else if (action.type === 'SET_VISIBILITY_FILTER') { return { ...state, visibilityFilter: action.filter }; } else { return state; } };
بر اساس مثال بالا:
todoAppReducer
از دستورهای if-else برای بررسی نوع اکشن (action.type
) و اجرای منطق متفاوت بر اساس نوع اکشن استفاده میکند.else
نهایی، state فعلی را بدون تغییر return میکند.مزایا: دستورات switch معمولاً هنگام مدیریت چندین نوع اکشن در reducerهای Redux قابل خواندن و نگهداری هستند. آنها به وضوح موارد مختلف را بر اساس انواع اکشن جدا میکنند.
ملاحظات: باید اطمینان حاصل کنیم که هر نوع اکشن دارای یک case
متناظر در عبارت switch باشد تا بتواند بهروزرسانیها را به درستی مدیریت کند.
مزایا: منطق شرطی (گزارههای if-else) انعطافپذیری را فراهم میکند و در سناریوهای خاصی که انواع اکشنهای کمتری وجود دارد، میتوانیم آن را آسانتر درک کنیم.
ملاحظات: در رسیدگی به انواع اکنشها سازگاری داشته باشیم و اطمینان حاصل کنیم که هر شرط، بهروزرسانیهای state را به درستی مدیریت میکند.
در عمل، دستورات switch به دلیل وضوح و قراردادی که در جامعه Redux دارند، رویکرد توصیه شده در reducerهای Redux هستند. آنها به حفظ یک رویکرد ساختاریافته برای مدیریت تغییرات state بر اساس انواع اکشنهای مختلف کمک میکنند و ثبات و پیشبینیپذیری را در برنامههای Redux ارتقا میدهند.
ارسال اکشنها در Redux برای مدیریت بهروزرسانیهای state برنامه ضروری است. Redux، یک state container قابل پیشبینی برای برنامههای جاوااسکریپت، متکی به اکشنهایی به عنوان محمولههای اطلاعاتی است که دادهها را از برنامه به Redux store ارسال میکند.
تابع dispatch
در Redux، روشی است که توسط Redux store ارائه شده است. از این تابع برای ارسال اکشنها برای ایجاد تغییرات state در برنامه استفاده میکنیم. هنگامی که یک اکشن ارسال میشود، Redux store تابع reducer مرتبط با آن را فراخوانی میکند، state جدید را محاسبه کرده و به همه subscriberها اطلاع میدهد که state بهروزرسانی شده است.
در ادامه روش استفاده از تابع dispatch
را بررسی میکنیم:
import { createStore } from 'redux'; // Reducer function const counterReducer = (state = { count: 0 }, action) => { switch (action.type) { case 'INCREMENT': return { ...state, count: state.count + 1 }; case 'DECREMENT': return { ...state, count: state.count - 1 }; default: return state; } }; // Create Redux store const store = createStore(counterReducer); // Dispatch actions to update state store.dispatch({ type: 'INCREMENT' }); store.dispatch({ type: 'DECREMENT' });
بر اساس این مثال:
createStore
یک Redux store ایجاد میکنیم و تابع counterReducer
را به آن پاس میدهیم.store.dispatch
برای ارسال اکشنها ({ type: 'INCREMENT' }
و { type: 'DECREMENT' }
) برای بهروزرسانی state استفاده میشود.در یک برنامه معمولی Redux، اکشنها اغلب در پاسخ به تعاملات کاربر یا eventهای دیگر از کامپوننتهای React ارسال میشوند.
برای ارسال اکشنها از کامپوننتها، معمولاً کامپوننت را با استفاده از تابع connect
React Redux یا هوکهایی مانند useDispatch
به Redux store متصل میکنیم.
در ادامه نحوه ارسال اکشنها از یک کامپوننت React با استفاده از تابع connect
و mapDispatchToProps
را داریم:
import React from 'react'; import { connect } from 'react-redux'; // Action creator functions const increment = () => ({ type: 'INCREMENT' }); const decrement = () => ({ type: 'DECREMENT' }); // Component definition const Counter = ({ count, increment, decrement }) => ( <div> <p>Count: {count}</p> <button onClick={increment}>Increment</button> <button onClick={decrement}>Decrement</button> </div> ); // Map state to props const mapStateToProps = (state) => ({ count: state.count }); // Map dispatch to props const mapDispatchToProps = { increment, decrement }; // Connect component to Redux store export default connect(mapStateToProps, mapDispatchToProps)(Counter);
مطابق مثال بالا:
increment
و decrement
توابع action creator هستند که اکشنها را return میکنند ({ type: 'INCREMENT' }
و { type: 'DECREMENT' }
).Counter
با استفاده از تابع connect
به Redux store متصل میشود. این کامپوننت count
را به عنوان prop، همراه با action creatorهای increment
و decrement
از Redux state دریافت میکند.از طرف دیگر، میتوانیم از هوکهای React Redux (useDispatch
) برای ارسال اکشنها در کامپوننتهای فانکشنال استفاده کنیم:
import React from 'react'; import { useDispatch, useSelector } from 'react-redux'; const Counter = () => { const count = useSelector(state => state.count); const dispatch = useDispatch(); const handleIncrement = () => { dispatch({ type: 'INCREMENT' }); }; const handleDecrement = () => { dispatch({ type: 'DECREMENT' }); }; return ( <div> <p>Count: {count}</p> <button onClick={handleIncrement}>Increment</button> <button onClick={handleDecrement}>Decrement</button> </div> ); }; export default Counter;
در این مثال کامپوننت فانکشنال:
useSelector
برای انتخاب count
از Redux store state استفاده میکنیم.useDispatch
برای دریافت تابع dispatch
از Redux store استفاده میکنیم.handleIncrement
و handleDecrement
اکشنها را ارسال میکنند ({ type: 'INCREMENT' }
و { type: 'DECREMENT' }
) تا هنگامی که بر روی دکمهها کلید میشود، Redux state بهروزرسانی گردد.دسترسی به دادههای خاص از store در Redux شامل پیمایش در ساختار state برنامه برای بازیابی اطلاعات دقیق مورد نیاز برای رندر کردن کامپوننتها یا اجرای منطق است.
Selectorها در Redux توابعی هستند که منطق بازیابی قطعات خاصی از داده از Redux store state را دربر میگیرند. آنها به جدا کردن کامپوننتها از ساختار state کمک میکنند و دسترسی کارآمد به دادهها و تبدیل آنها را تسهیل مینمایند.
در ادامه نحوه ایجاد توابع selector را بررسی میکنیم:
// Example Redux state const initialState = { todos: [ { id: 1, text: 'Learn Redux', completed: false }, { id: 2, text: 'Write Redux selectors', completed: true }, // more todos... ], visibilityFilter: 'SHOW_COMPLETED' }; // Selector function to get todos from state const getTodos = (state) => state.todos; // Selector function to filter todos based on visibility filter const getVisibleTodos = (state) => { const todos = getTodos(state); const visibilityFilter = state.visibilityFilter; switch (visibilityFilter) { case 'SHOW_COMPLETED': return todos.filter(todo => todo.completed); case 'SHOW_ACTIVE': return todos.filter(todo => !todo.completed); case 'SHOW_ALL': default: return todos; } };
با توجه به این مثال:
getTodos
یک تابع selector است که آرایه todos
را از Redux state بازیابی میکند.getVisibleTodos
یک تابع selector است که todos
را بر اساس visibilityFilter
ذخیره شده در state فیلتر میکند.Selectorها همچنین میتوانند برای ایجاد selectorهای پیچیدهتر باهم ترکیب شوند:
// Composed selector function to get visible todos const getVisibleTodos = (state) => { const todos = getTodos(state); const visibilityFilter = state.visibilityFilter; switch (visibilityFilter) { case 'SHOW_COMPLETED': return getCompletedTodos(todos); case 'SHOW_ACTIVE': return getActiveTodos(todos); case 'SHOW_ALL': default: return todos; } }; // Helper functions for filtering todos const getCompletedTodos = (todos) => todos.filter(todo => todo.completed); const getActiveTodos = (todos) => todos.filter(todo => !todo.completed);
Memoization تکنیکی است که برای بهینهسازی محاسبات پرهزینه از طریق ذخیرهسازی نتایج فراخوانی تابع بر اساس ورودی آنها استفاده میشود. در context مربوط بهselectorهای Redux، مفهوم memoization میتواند عملکرد را با اطمینان از اینکه selectorها تنها زمانی که ورودی (state)شان تغییر میکند، دوباره محاسبه میکنند، بهبود بخشد.
میتوانیم از کتابخانههایی مانند reselect
برای memoization در selectorهای Redux استفاده کنیم:
npm install reselect
مثال استفاده از reselect
برای memoization:
import { createSelector } from 'reselect'; // Selectors const getTodos = (state) => state.todos; const getVisibilityFilter = (state) => state.visibilityFilter; // Memoized selector to get visible todos const getVisibleTodos = createSelector( [getTodos, getVisibilityFilter], (todos, visibilityFilter) => { switch (visibilityFilter) { case 'SHOW_COMPLETED': return todos.filter(todo => todo.completed); case 'SHOW_ACTIVE': return todos.filter(todo => !todo.completed); case 'SHOW_ALL': default: return todos; } } );
با دقت در مثال بالا در مییابیم که:
createSelector
از reselect
یک memoized selector ایجاد میکند که getTodos
و getVisibilityFilter
را به عنوان selectorهای ورودی میگیرد.visibilityFilter
محاسبه میکند و نتیجه را تا زمانی که selectorهای ورودی تغییر کنند در حافظه cache ذخیره میکند.اتصال کامپوننتهای React به Redux یک تکنیک اساسی برای مدیریت کارآمد state برنامه در پروژههای مبتنی بر React است. Redux به عنوان یک store متمرکز عمل میکند که state کل برنامه را نگه میدارد و برای هر کامپوننتی که به آن نیاز دارد قابل دسترسی میباشد.
در برنامههای React که از Redux برای مدیریت state استفاده میکنند، تابع connect
از کتابخانه react-redux
برای اتصال کامپوننتهای React به Redux store استفاده میشود.
نحوه استفاده از تابع connect
به صورت زیر میباشد:
import React from 'react'; import { connect } from 'react-redux'; // Define a React component const Counter = ({ count, increment, decrement }) => ( <div> <p>Count: {count}</p> <button onClick={increment}>Increment</button> <button onClick={decrement}>Decrement</button> </div> ); // Map Redux state to component props const mapStateToProps = (state) => ({ count: state.count }); // Map dispatching actions to component props const mapDispatchToProps = { increment: () => ({ type: 'INCREMENT' }), decrement: () => ({ type: 'DECREMENT' }) }; // Connect component to Redux store export default connect(mapStateToProps, mapDispatchToProps)(Counter);
mapStateToProps
: این تابع state مربوط به Redux store را به کامپوننتهای React ما map میکند. Redux state را به عنوان آرگومان میگیرد و یک آبجکت را return میکند. هر فیلد در آبجکت بازگشتی به یک prop برای کامپوننت متصل تبدیل میشود.
mapDispatchToProps
: این تابع اکشنهای ارسالی را به props کامپوننت React ما map میکند. این میتواند یک آبجکت باشد که در آن هر فیلد یک تابع action creator است، یا تابعی که dispatch
را به عنوان آرگومان دریافت کرده و یک آبجکت را return میکند. هر action creatorای به طور خودکار با dispatch
wrapp میشود تا بتوانیم آنها را مستقیماً فراخوانی کنیم.
در مثالی که داشتیم:
mapStateToProps
فیلد count
را از Redux state (state.count
) به prop count
کامپوننت Counter
map میکند.mapDispatchToProps
اکشنهای increment
و decrement
را به props map میکند، بنابراین با کلیک کردن روی دکمههای کامپوننت Counter
، اکشنهای مربوطه ارسال میشود ({ type: 'INCREMENT' }
و { type: 'DECREMENT' }
).هنگامی که یک کامپوننت با استفاده از تابع connect
به Redux store متصل میشود، میتواند به Redux state دسترسی داشته باشد و اکشنها را از طریق props ارسال کند. در ادامه نحوه استفاده از کامپوننتهای متصل در برنامه خود را بررسی میکنیم:
import React from 'react'; import ReactDOM from 'react-dom'; import { Provider } from 'react-redux'; import { createStore } from 'redux'; import rootReducer from './reducers'; // Import your root reducer import App from './App'; // Import your connected component // Create Redux store with root reducer const store = createStore(rootReducer); // Render the App component inside the Provider ReactDOM.render( <Provider store={store}> <App /> </Provider>, document.getElementById('root') );
در این تنظیمات:
Provider
کامپوننتی از react-redux
است که Redux store را برای هر کامپوننت تودرتویی که با connect
متصل شده است، در دسترس قرار میدهد.store
با استفاده از createStore
ایجاد میشود و با یک root reducer (rootReducer
) ترکیب میشود که همه reducerهای ما را در یک reducer ترکیب میکند.با قرار دادن کامپوننت سطح بالای خود (در این مثال App
میباشد) داخل Provider
و ارسال Redux store به عنوان یک prop، همه کامپوننتهای متصل در برنامه ما میتوانند به Redux store دسترسی داشته باشند و از طریق props با آن تعامل برقرار نمایند (mapStateToProps
و mapDispatchToProps
mappingها).
تکنیکهای پیشرفته جریان داده Redux بر اصول اساسی مدیریت state در برنامههای پیچیده گسترش مییابد. این تکنیکها فراتر از مفهوم اکشنهای بیسیک و reducerها هستند و مفاهیمی مانند middleware، selectorها و اکشنهای asynchronous را معرفی میکنند.
در Redux، مدیریت اکشنهای asynchronous شامل مدیریت اکشنهایی است که دارای side effect هستند، مانند دریافت داده از یک سرور یا بهروزرسانی state به صورت asynchronous. ریداکس چندین راهحل middleware را برای مدیریت مؤثر اکشنهای asynchronous ارائه میکند.
Redux Thunk یک middleware است که به ما اجازه میدهد تا action creatorهایی بنویسیم که به جای یک action object، یک تابع را return میکند. سپس این تابع میتواند عملیات asynchronous را انجام دهد و پس از تکمیل عملیات asynchronous، اکشنهای synchronous منظم را ارسال نماید.
در ادامه مثالی از روش استفاده از Redux Thunk برای اکشنهای asynchronous را داریم:
راهاندازی Redux Thunk Middleware:
import { createStore, applyMiddleware } from 'redux'; import thunk from 'redux-thunk'; import rootReducer from './reducers'; // Import your root reducer // Create Redux store with thunk middleware const store = createStore(rootReducer, applyMiddleware(thunk));
Async Action Creator با استفاده از Redux Thunk:
// Action creator function using Redux Thunk const fetchPosts = () => { return async (dispatch) => { dispatch({ type: 'FETCH_POSTS_REQUEST' }); try { const response = await fetch('https://jsonplaceholder.typicode.com/posts'); const posts = await response.json(); dispatch({ type: 'FETCH_POSTS_SUCCESS', payload: posts }); } catch (error) { dispatch({ type: 'FETCH_POSTS_FAILURE', error: error.message }); } }; };
با توجه به مثال بالا:
fetchPosts
یک action creator است که به جای یک action object، یک تابع را return میکند.Redux Saga یکی دیگر از middlewareها برای مدیریت side effectها در برنامههای Redux است. این middleware از generatorهای ES6 برای آسانتر خواندن، نوشتن و تست کدهای asynchronous استفاده میکند.
در ادامه مثالی از روش استفاده از Redux Saga برای مدیریت اکشنهای asynchronous را داریم:
راه اندازی Redux Saga Middleware:
import { createStore, applyMiddleware } from 'redux'; import createSagaMiddleware from 'redux-saga'; import rootReducer from './reducers'; // Import your root reducer import rootSaga from './sagas'; // Import your root saga // Create Redux Saga middleware const sagaMiddleware = createSagaMiddleware(); // Create Redux store with Saga middleware const store = createStore(rootReducer, applyMiddleware(sagaMiddleware)); // Run the root saga sagaMiddleware.run(rootSaga);
مثال Saga (rootSaga.js):
import { all, call, put, takeEvery } from 'redux-saga/effects'; import { fetchPostsSuccess, fetchPostsFailure } from './actions'; // Import your action creators // Worker saga for fetching posts function* fetchPostsSaga() { try { const response = yield call(fetch, 'https://jsonplaceholder.typicode.com/posts'); const posts = yield call([response, 'json']); yield put(fetchPostsSuccess(posts)); } catch (error) { yield put(fetchPostsFailure(error.message)); } } // Watcher saga to listen for FETCH_POSTS_REQUEST action function* watchFetchPosts() { yield takeEvery('FETCH_POSTS_REQUEST', fetchPostsSaga); } // Root saga export default function* rootSaga() { yield all([ watchFetchPosts() // Add more watchers if needed ]); }
با توجه به مثال بالا:
fetchPostsSaga
یک worker saga است که عملیات asynchronous (دریافت پستها) را انجام میدهد.watchFetchPosts
یک watcher saga است که به اکشنهای خاص (FETCH_POSTS_REQUEST
) گوش میدهد و worker saga مربوطه را راهاندازی میکند.rootSaga
چندین saga را با استفاده از all
ترکیب کرده و آنها را با استفاده از sagaMiddleware.run
اجرا مینماید.Middleware در Redux راهی برای گسترش قابلیتهای Redux store، مانند اکشنهای logging، مدیریت عملیات asynchronous، عملیات routing و غیره ارائه میکند. این middleware مابین ارسال یک اکشن و لحظهای که به reducer میرسد قرار میگیرد و اجازه رهگیری و دستکاری اکشنها را میدهد.
نمونهای از middlewareهای سفارشی:
const loggerMiddleware = store => next => action => { console.log('Dispatching action:', action); const result = next(action); console.log('New state:', store.getState()); return result; }; // Applying custom middleware to Redux store import { createStore, applyMiddleware } from 'redux'; import rootReducer from './reducers'; // Import your root reducer // Create Redux store with custom middleware const store = createStore(rootReducer, applyMiddleware(loggerMiddleware));
بر اساس آن چه که در مثال بالا میبینیم:
loggerMiddleware
یک تابع middleware سفارشی است که هر اکشن ارسال شده و state حاصل را ثبت میکند.next
تابعی است که توسط Redux ارائه شده است که اجازه میدهد تا اکشن به middleware بعدی یا reducer ادامه پیدا کند.Redux روشی ساختاریافته برای مدیریت state در برنامههای جاوااسکریپت ارائه میکند، اما استفاده مؤثر مستلزم رعایت بهترین شیوهها میباشد. این مقاله چند توصیه کلیدی برای مدیریت جریان داده در Redux در اختیار ما قرار میدهد که عبارتند از:
ساختار و سازماندهی فایل:
src/ ├── actions/ │ ├── todosActions.js │ └── userActions.js ├── reducers/ │ ├── todosReducer.js │ └── userReducer.js ├── selectors/ │ ├── todosSelectors.js │ └── userSelectors.js └── store.js
Action Typeها:
// Action types export const ADD_TODO = 'ADD_TODO'; export const DELETE_TODO = 'DELETE_TODO';
ترکیب Reducer:
combineReducers
در Redux استفاده کنیم.import { combineReducers } from 'redux'; import todosReducer from './todosReducer'; import userReducer from './userReducer'; const rootReducer = combineReducers({ todos: todosReducer, user: userReducer }); export default rootReducer;
تغییرناپذیری با Spread Operator:
...
): بهتر است هنگام بهروزرسانی state، آبجکتها یا آرایههای جدید ایجاد کنیم تا تغییرناپذیری حفظ شود.// Updating an array in Redux state const todosReducer = (state = initialState, action) => { switch (action.type) { case ADD_TODO: return { ...state, todos: [ ...state.todos, { id: action.id, text: action.text, completed: false } ] }; case TOGGLE_TODO: return { ...state, todos: state.todos.map(todo => (todo.id === action.id) ? { ...todo, completed: !todo.completed } : todo ) }; default: return state; } };
کتابخانههای تغییرناپذیر:
Immutable.js
را نیز در نظر بگیریم.import { Map, List } from 'immutable'; const initialState = Map({ todos: List(), user: Map() }); const todosReducer = (state = initialState, action) => { switch (action.type) { case ADD_TODO: return state.update('todos', todos => todos.push(Map({ id: action.id, text: action.text, completed: false }))); case TOGGLE_TODO: return state.update('todos', todos => todos.map(todo => (todo.get('id') === action.id) ? todo.set('completed', !todo.get('completed')) : todo ) ); default: return state; } };
Unit Testing:
describe('todosReducer', () => { it('should handle ADD_TODO', () => { const action = { type: 'ADD_TODO', id: 1, text: 'Test todo' }; const initialState = { todos: [] }; const expectedState = { todos: [{ id: 1, text: 'Test todo', completed: false }] }; expect(todosReducer(initialState, action)).toEqual(expectedState); }); });
تست یکپارچهسازی:
describe('fetchPosts action creator', () => { it('creates FETCH_POSTS_SUCCESS when fetching posts has been done', () => { const expectedActions = [ { type: 'FETCH_POSTS_REQUEST' }, { type: 'FETCH_POSTS_SUCCESS', payload: { /* mocked data */ } } ]; const store = mockStore({ posts: [] }); return store.dispatch(fetchPosts()).then(() => { expect(store.getActions()).toEqual(expectedActions); }); }); });
یکپارچهسازی با کامپوننتها:
redux-mock-store
برای شبیهسازی رفتار Redux store تست کنیم.import configureStore from 'redux-mock-store'; import { Provider } from 'react-redux'; import { render } from '@testing-library/react'; import App from './App'; const mockStore = configureStore([]); describe('<App />', () => { it('renders App component', () => { const store = mockStore({ /* mocked state */ }); const { getByText } = render( <Provider store={store}> <App /> </Provider> ); expect(getByText('Welcome to Redux App')).toBeInTheDocument(); }); });
Redux یک راهحل قدرتمند مدیریت state برای برنامههای جاوااسکریپت ارائه میدهد و یک روش قابل پیشبینی و متمرکز برای مدیریت state برنامه فراهم میکند.
Redux ما را قادر میسازد تا یا با مدیریت عملیات asynchronous با middlewareهایی مانند Redux Thunk یا Redux Saga، یا با بهینهسازی مدیریت state از طریق شیوههای دادههای تغییرناپذیر، برنامههای مقیاسپذیر و قابل نگهداری بسازیم.
با تسلط بر این تکنیکها، میتوانیم از Redux برای سادهسازی جریان داده، بهبود عملکرد برنامهها و سادهسازی پیچیدگیهای مدیریت state در توسعه وب مدرن استفاده نماییم.