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

Redux چیست؟

Redux یک state container قابل پیش‌بینی برای برنامه‌های جاوااسکریپت است که در درجه اول با کتابخانه‌هایی مانند React مورد استفاده قرار می‌گیرد. این container به مدیریت state برنامه در یک store متمرکز کمک می‌کند و مدیریت و به‌روزرسانی state را در کل برنامه آسان‌تر می‌نماید.

به زبان ساده، Redux راهی برای ذخیره‌سازی و مدیریت داده‌هایی که برنامه ما برای کار به آن‌ها نیاز دارد، ارائه می‌دهد. همچنین از یک الگوی دقیق پیروی می‌کند تا اطمینان حاصل شود که تغییرات state قابل پیش‌بینی و مدیریت هستند.

چرا باید از Redux برای مدیریت داده استفاده کنیم؟

استفاده از Redux برای مدیریت داده در برنامه‌های خود چندین مزیت دارد:

مفاهیم اصلی جریان داده در Redux

درک مفاهیم اصلی جریان داده در Redux برای تسلط بر مدیریت state در برنامه‌های جاوااسکریپت مدرن ضروری است.

جریان داده یک طرفه

Redux از یک الگوی جریان داده دقیق یک طرفه پیروی می‌کند. به این معنی که داده‌ها در برنامه ما از طریق یک سری مراحل در یک جهت حرکت می‌کنند:

  1. Actionها: اکشن‌ها آبجکت‌های جاوااسکریپت ساده‌ای هستند که قصد تغییر state را نشان می‌دهند. آن‌ها تنها منبع اطلاعاتی برای store به‌شمار می‌آیند.
  2. Reducerها: Reducerها توابع pure هستند که مسئولیت مدیریت انتقال state بر اساس اکشن‌ها را بر عهده دارند. آن‌ها مشخص می‌کنند که state برنامه در پاسخ به اکشن‌های ارسال شده به store چگونه تغییر می‌کند.
  3. Store: store وظیفه دارد تا state برنامه را نگه دارد. این مفهوم امکان دسترسی به state را از طریق getState()، به‌روزرسانی state از طریق dispatch(action) و ثبت listenerها از طریق subscribe(listener) را فراهم می‌کند.
  4. View: کامپوننت‌های React (یا هر لایه UI دیگری) در store سابسکرایب می‌شوند تا در صورت تغییر state، به‌روزرسانی‌ها را دریافت کنند. سپس بر اساس state به‌روزرسانی شده، دوباره رندر می‌شوند.

در ادامه یک نمای کلی ساده از نحوه عملکرد جریان داده یک طرفه در Redux را داریم:

  1. Action Dispatch: کامپوننت‌ها با استفاده از store.dispatch(action) اکشن‌ها را به Redux store ارسال می‌کنند. اکشن‌ها آبجکت‌های جاوااسکریپت ساده با یک فیلد type هستند که نوع عمل انجام شده را توصیف می‌کند.
  2. Action Handling: سپس store اکشن ارسال شده را به root reducer منتقل می‌کند. reducer یک تابع pure است که state فعلی و اکشن را می‌گیرد، state جدید را بر اساس اکشن محاسبه کرده و state به‌روزرسانی شده را return می‌کند.
  3. به‌روزرسانی state: پس از آن Redux store، state خود را بر اساس مقدار بازگشتی root reducer به‌روزرسانی می‌کند. همه کامپوننت‌های سابسکرایب شده را از تغییر state مطلع می‌شوند.
  4. رندر مجدد کامپوننت: کامپوننت‌هایی که در store سابسکرایب شده‌اند state به‌روزرسانی شده را به عنوان props دریافت می‌کنند. سپس با داده‌های جدید دوباره رندر می‌شوند.

مزایای جریان داده یک طرفه

مدیریت state با Redux Store

مدیریت state نقش بسیار مهم و اساسی در توسعه وب مدرن ایفا می‌کند و تضمین می‌نماید که اپلیکیشن‌ها، stateهای سازگار و قابل پیش‌بینی را در بین کامپوننت‌های مختلف حفظ می‌کنند.

Redux Store چیست؟

Store مهم‌ترین قسمت مدیریت state با Redux است و کل درخت state برنامه را نگه می‌دارد. store به ما اجازه می‌دهد تا:

در اصل، Redux Store به عنوان یک مخزن متمرکز برای state برنامه ما عمل می‌کند، جریان داده قابل پیش‌بینی را تسهیل کرده و مدیریت state را آسان‌تر می‌کند.

بررسی ساختار Store (State، Reducerها و Actionها)

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 برنامه در کل برنامه ارائه می‌دهند.

Actionها: آغاز تغییرات state

مدیریت state به طور موثر در هسته ایجاد اپلیکیشن‌های پویا و ریسپانسیو نهفته است. اکشن‌ها، در معماری Redux و کتابخانه‌های مدیریت state مشابه، به عنوان المنت‌های مهم برای شروع تغییرات state  عمل می‌کنند.

Action Creatorها

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');

باتوجه به  مثال بالا:

به طور کلی، action creatorها فرآیند ایجاد اکشن‌ها را ساده می‌کنند. به‌ویژه زمانی که اکشن‌ها قبل از ارسال، به داده‌ها یا محاسبات پیچیده نیاز دارند.

Action typeها

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;
  }
};

مطابق آنچه که در مثال بالا دیدیم:

نحوه پردازش تغییرات state

در قلب مدیریت state، مفهوم reducerها طراحی شده است تا انتقال state را به شیوه‌ای کنترل‌شده و تغییرناپذیر مدیریت کند.

توابع pure

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;
  }
};

با توجه به مثال بالا:

به‌روزرسانی تغییرناپذیر state: توجه به این نکته لازم است که reducerها هرگز نباید مستقیماً state را تغییر دهند. در عوض، کپی‌هایی از state ایجاد می‌کنند و آن‌ها را برای تولید یک آبجکت جدید تغییر می‌دهند. این موضوع تضمین می‌کند که Redux می‌تواند تغییرات state را تشخیص دهد و کامپوننت‌ها را به‌طور مؤثر به‌روزرسانی نماید.

اصل مسئولیت منفرد: هر reducer معمولاً به‌روزرسانی‌های یک بخش خاص از state برنامه را مدیریت می‌کند. این موضوع به تفکیک واضح نگرانی‌ها کمک می‌کند و درک، تست و نگه‌داری reducerها را آسان‌تر می‌نماید.

ویژگی‌های توابع Pure

توابع pure، از جمله reducerهای Redux، دارای ویژگی‌های خاصی هستند که آن‌ها را برای مدیریت تغییرات state مناسب می‌کند:

قطعی بودن: یک تابع pure همیشه خروجی یکسانی را برای ورودی یکسان تولید می‌کند. این پیش‌بینی‌پذیری تضمین می‌کند که reducerها به‌طور مداوم این گونه رفتار می‌کنند، در نتیجه استدلال کردن در مورد آن آسان‌تر است.

بدون Side Effect بودن: توابع pure آرگومان‌های ورودی یا هیچ state خارجی را تغییر نمی‌دهند. آن‌ها فقط به پارامترهای ورودی خود وابستگی دارند و بدون ایجاد side effect قابل مشاهده، خروجی تولید می‌کنند.

داده‌های غیرقابل تغییر: توابع pure داده‌ها را تغییر نمی‌دهند. در عوض، آن‌ها ساختارهای داده‌ای جدیدی را ایجاد و return می‌کنند. در Redux، reducerها یک آبجکت state جدید را بدون تغییر state موجود، تولید می‌کنند که تشخیص کارآمد تغییرات و مدیریت state را ممکن می‌سازد.

شفافیت ارجاعی: می‌توانیم توابع pure را بدون اینکه بر صحت برنامه‌ای که داریم تأثیر بگذارد با مقادیر بازگشتی خود جایگزین کنیم. این ویژگی از ترکیب‌پذیری پشتیبانی کرده و تست و استدلال در مورد کد را آسان‌تر می‌کند.

ساختار یک تابع Reducer

تابع reducer، در هسته خود، نحوه تغییر state برنامه در پاسخ به اکشن‌های ارسال شده را تعریف می‌کند. این تابع دو پارامتر می‌گیرد: state فعلی و یک action object که state جدید را بر اساس نوع اکشن دریافت شده تعیین می‌نماید.

پارامترها: state قبلی و Action Object

یک تابع 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 اضافه کنیم.

مقدار بازگشتی: 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;
  }
};

بر اساس مثال بالا:

نکات کلیدی:

به‌روزرسانی غیرقابل تغییر: reducerها هرگز نباید مستقیماً state قبلی را تغییر دهند. در عوض، با کپی کردن state قبلی (...state) و اعمال تغییرات در آن، یک آبجکت جدید ایجاد می‌کنند.

حالت پیش‌فرض: اگر reducer نوع اکشن را تشخیص ندهد، case default در دستور switch state فعلی را بدون تغییر return می‌کند. این موضوع تضمین می‌کند که reducer همیشه یک state object معتبر را return می‌کند، حتی اگر هیچ تغییری ایجاد نشود.

مسئولیت منفرد: هر case در دستور switch مربوط به یک نوع اکشن خاص است و مسئول به‌روزرسانی یک بخش خاص از state برنامه می‌باشد. این امر باعث تفکیک واضح نگرانی‌ها شده و درک و نگه‌داری reducerها را آسان‌تر می‌کند.

نحوه انجام اکشن‌های مختلف در Reducerها

ما در Redux می‌توانیم با استفاده از دستورهای switch یا منطق شرطی، اکشن‌های مختلفی را در reducerها انجام دهیم. هدف هر دو رویکرد تعیین چگونگی تغییر state برنامه بر اساس نوع اکشن ارسال شده می‌باشد.

استفاده از دستورات switch

دستورات 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;
  }
};

با توجه این مثال:

استفاده از منطق شرطی

از طرف دیگر، 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;
  }
};

بر اساس مثال بالا:

انتخاب بین دستورات switch و منطق شرطی

دستورات Switch:

مزایا: دستورات switch معمولاً هنگام مدیریت چندین نوع اکشن در reducerهای Redux قابل خواندن و نگه‌داری هستند. آن‌ها به وضوح موارد مختلف را بر اساس انواع اکشن جدا می‌کنند.

ملاحظات: باید اطمینان حاصل کنیم که هر نوع اکشن دارای یک case متناظر در عبارت switch باشد تا بتواند به‌روزرسانی‌ها را به درستی مدیریت کند.

منطق شرطی:

مزایا: منطق شرطی (گزاره‌های if-else) انعطاف‌پذیری را فراهم می‌کند و در سناریوهای خاصی که انواع اکشن‌های کم‌تری وجود دارد، می‌توانیم آن را آسان‌تر درک کنیم.

ملاحظات: در رسیدگی به انواع اکنش‌ها سازگاری داشته باشیم و اطمینان حاصل کنیم که هر شرط، به‌روزرسانی‌های state را به درستی مدیریت می‌کند.

در عمل، دستورات switch به دلیل وضوح و قراردادی که در جامعه Redux دارند، رویکرد توصیه شده در reducerهای Redux هستند. آن‌ها به حفظ یک رویکرد ساختاریافته برای مدیریت تغییرات state بر اساس انواع اکشن‌های مختلف کمک می‌کنند و ثبات و پیش‌بینی‌پذیری را در برنامه‌های Redux ارتقا می‌دهند.

ارسال Actionها: نحوه به‌روزرسانی Redux Store

ارسال اکشن‌ها در Redux برای مدیریت به‌روزرسانی‌های state برنامه ضروری است. Redux، یک state container قابل پیش‌بینی برای برنامه‌های جاوااسکریپت، متکی به اکشن‌هایی به عنوان محموله‌های اطلاعاتی است که داده‌ها را از برنامه به Redux store ارسال می‌کند.

تابع dispatch

تابع 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' });

بر اساس این مثال:

ارسال اکشن‌ها از کامپوننت‌ها یا Eventها

در یک برنامه معمولی 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);

مطابق مثال بالا:

از طرف دیگر، می‌توانیم از هوک‌های 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;

در این مثال کامپوننت فانکشنال:

نحوه دسترسی به داده‌های خاص از store

دسترسی به داده‌های خاص از store در Redux شامل پیمایش در ساختار state برنامه برای بازیابی اطلاعات دقیق مورد نیاز برای رندر کردن کامپوننت‌ها یا اجرای منطق است.

ایجاد توابع Selector

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;
  }
};

با توجه به این مثال:

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 برای استفاده کارآمد از Selector

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;
    }
  }
);

با دقت در مثال بالا در می‌یابیم که:

نحوه اتصال کامپوننت‌های React به Redux

اتصال کامپوننت‌های React به Redux یک تکنیک اساسی برای مدیریت کارآمد state برنامه در پروژه‌های مبتنی بر React است. Redux به عنوان یک store متمرکز عمل می‌کند که state کل برنامه را نگه می‌دارد و برای هر کامپوننتی که به آن نیاز دارد قابل دسترسی می‌باشد.

تابع connect از کتابخانه react-redux

در برنامه‌های 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);

Mapping State و ارسال به Props

mapStateToProps: این تابع state مربوط به Redux store را به کامپوننت‌های React ما map می‌کند. Redux state را به عنوان آرگومان می‌گیرد و یک آبجکت را return می‌کند. هر فیلد در آبجکت بازگشتی به یک prop برای کامپوننت متصل تبدیل می‌شود.

mapDispatchToProps: این تابع اکشن‌های ارسالی را به props کامپوننت React ما map می‌کند. این می‌تواند یک آبجکت باشد که در آن هر فیلد یک تابع action creator است، یا تابعی که dispatch را به عنوان آرگومان دریافت کرده و یک آبجکت را return می‌کند. هر action creatorای به طور خودکار با dispatch wrapp می‌شود تا بتوانیم آن‌ها را مستقیماً فراخوانی کنیم.

در مثالی که داشتیم:

استفاده از کامپوننت‌های متصل در برنامه ما

هنگامی که یک کامپوننت با استفاده از تابع 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')
);

در این تنظیمات:

با قرار دادن کامپوننت سطح بالای خود (در این مثال App می‌باشد) داخل Provider و ارسال Redux store به عنوان یک prop، همه کامپوننت‌های متصل در برنامه ما می‌توانند به Redux store دسترسی داشته باشند و از طریق props با آن تعامل برقرار نمایند (mapStateToProps و mapDispatchToProps mappingها).

تکنیک‌های پیشرفته جریان داده Redux

تکنیک‌های پیشرفته جریان داده Redux بر اصول اساسی مدیریت state در برنامه‌های پیچیده گسترش می‌یابد. این تکنیک‌ها فراتر از مفهوم اکشن‌های بیسیک و reducerها هستند و مفاهیمی مانند middleware، selectorها و اکشن‌های asynchronous را معرفی می‌کنند.

اکشن‌های asynchronous 

در Redux، مدیریت اکشن‌های asynchronous شامل مدیریت اکشن‌هایی است که دارای side effect هستند، مانند دریافت داده از یک سرور یا به‌روزرسانی state به صورت asynchronous. ریداکس چندین راه‌حل middleware را برای مدیریت مؤثر اکشن‌های asynchronous ارائه می‌کند.

Redux Thunk

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 });
    }
  };
};

با توجه به مثال بالا:

Redux Saga

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
  ]);
}

با توجه به مثال بالا:

Middleware برای گسترش فانکشنالیتی Redux

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));

بر اساس آن چه که در مثال بالا می‌بینیم:

بهترین روش‌ها برای مدیریت جریان داده در Redux

Redux روشی ساختاریافته برای مدیریت state در برنامه‌های جاوااسکریپت ارائه می‌کند، اما استفاده مؤثر مستلزم رعایت بهترین شیوه‌ها می‌باشد. این مقاله چند توصیه کلیدی برای مدیریت جریان داده در Redux در اختیار ما قرار می‌دهد که عبارتند از:

سازماندهی Reducerها و Actionها

ساختار و سازماندهی فایل:

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:

import { combineReducers } from 'redux';
import todosReducer from './todosReducer';
import userReducer from './userReducer';

const rootReducer = combineReducers({
  todos: todosReducer,
  user: userReducer
});

export default rootReducer;

به‌روزرسانی تغییرناپذیر state

تغییرناپذیری با Spread Operator:

// 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;
  }
};

کتابخانه‌های تغییرناپذیر:

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;
  }
};

تست برنامه‌های Redux

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);
    });
  });
});

یکپارچه‌سازی با کامپوننت‌ها:

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 در توسعه وب مدرن استفاده نماییم.