توثيق Redux الشامل
دليل مفصل وشامل للعمل مع Redux من البداية إلى الاحتراف
فهرس المحتويات
١. مقدمة لـ Redux
ما هو Redux ولماذا نستخدمه؟ المشاكل التي يحلها ومتى يكون مناسباً.
٢. المفاهيم الأساسية
فهم المفاهيم الثلاثة الأساسية: Store، Actions، Reducers.
٣. إعداد مشروع Redux
خطوات تثبيت وإعداد Redux في مشروع جديد أو قائم.
٤. إنشاء المتجر (Store)
تكوين وإعداد متجر Redux مع الشروحات التفصيلية.
٥. الإجراءات (Actions)
إنشاء الإجراءات وموزعات الإجراءات وأفضل الممارسات.
٦. المخفضات (Reducers)
كتابة المخفضات النقية وتجميعها وإدارة الحالة.
٧. Redux مع React
ربط Redux مع تطبيقات React باستخدام React-Redux.
٨. Redux Toolkit
استخدام Redux Toolkit لتبسيط وتحسين تجربة التطوير.
٩. التعامل مع الآثار الجانبية
استخدام Middleware وThunks والنماذج المختلفة للآثار الجانبية.
١. مقدمة لـ Redux
ما هو Redux؟
Redux هو مكتبة JavaScript لإدارة حالة التطبيق، وهو مستوحى من Flux وElm. يقدم حلاً موحداً لإدارة الحالة في تطبيقات الويب، خاصة التطبيقات المبنية بـ React، على الرغم من أنه يمكن استخدامه مع أي إطار عمل أو مكتبة JavaScript أخرى.
الخلاصة: Redux هو مكتبة لإدارة الحالة في تطبيقات JavaScript. يساعد على تنظيم كود التطبيق وجعل تدفق البيانات أكثر قابلية للتنبؤ.
لماذا نستخدم Redux؟
١. إدارة الحالة المعقدة
يسهل Redux إدارة حالة التطبيق عندما تصبح معقدة وتحتاج إلى مشاركتها بين مكونات متعددة ومتباعدة في شجرة المكونات.
٢. قابلية التنبؤ
يفرض Redux تدفق بيانات أحادي الاتجاه وقواعد صارمة لتغيير الحالة، مما يجعل سلوك التطبيق أكثر قابلية للتنبؤ.
٣. قابلية التتبع والتصحيح
يسهل Redux تتبع متى وأين وكيف ولماذا تم تغيير حالة التطبيق، مما يسهل اكتشاف الأخطاء وتصحيحها.
٤. الفصل بين المكونات
يساعد على فصل منطق إدارة البيانات عن مكونات واجهة المستخدم، مما يؤدي إلى كود أكثر نظافة وقابلية للصيانة.
متى نستخدم Redux؟
لا يُعد Redux ضرورياً في كل مشروع. فيما يلي بعض المؤشرات التي تدل على أن استخدام Redux قد يكون مفيداً:
- عندما يكون لديك حالة معقدة تؤثر على أجزاء مختلفة من التطبيق
- عندما تحتاج مكونات متعددة غير مرتبطة مباشرة إلى الوصول إلى نفس البيانات
- عندما يكون لديك تدفقات عمل معقدة أو تسلسلات إجراءات
- عندما يصبح نقل الحالة عبر المكونات ضعيفة الارتباط أمراً صعباً (prop drilling)
- عندما تكون مشاركة البيانات بين مكونات مختلفة ضرورية لوظائف تطبيقك
نصيحة: لا تبدأ بـ Redux فقط لأنه شائع. في المشاريع الصغيرة، قد تكون أدوات مثل Context API أو useReducer كافية ولا تتطلب بنية تحتية إضافية.
٢. المفاهيم الأساسية في Redux
يعتمد Redux على ثلاثة مفاهيم أساسية تشكل بنيته: المتجر (Store)، الإجراءات (Actions)، والمخفضات (Reducers). فهم هذه المفاهيم ضروري للعمل مع Redux.
المتجر (Store)
المتجر هو مستودع الحالة الشامل للتطبيق. يمثل "حقيقة واحدة" تحفظ جميع بيانات التطبيق في هيكل شجري واحد.
- يحفظ حالة التطبيق
- يتيح قراءة الحالة
- يسمح بتحديث الحالة (عبر إرسال الإجراءات)
- يسمح بالاشتراك لتلقي إشعارات عند تغيير الحالة
الإجراءات (Actions)
الإجراءات هي كائنات JavaScript تصف ما حدث في التطبيق. تحتوي على حقل type
إلزامي وبيانات اختيارية.
- الطريقة الوحيدة لإرسال معلومات إلى المتجر
- وصف للتغييرات المطلوبة
- عادة ما تكون صغيرة ووصفية
- يتم إنشاؤها عبر دوال موزعة (action creators)
المخفضات (Reducers)
المخفضات هي دوال نقية تأخذ الحالة السابقة وإجراء، وتعيد حالة جديدة. تحدد كيفية تغيير حالة التطبيق استجابة للإجراءات.
- دوال نقية بدون آثار جانبية
- لا تعدل الحالة الحالية، بل تنشئ نسخة جديدة
- تحلل نوع الإجراء لتحديد كيفية إجراء التغييرات
- يمكن تقسيمها وتجميعها لإدارة أجزاء مختلفة من الحالة
تدفق البيانات في Redux
تدفق البيانات في Redux أحادي الاتجاه، مما يسهل فهم وتتبع التغييرات في التطبيق:
-
١
المستخدم يتفاعل مع التطبيق
مثلاً، المستخدم ينقر على زر "إضافة إلى السلة"
-
٢
التطبيق يرسل إجراءً (Action)
يتم استدعاء دالة موزعة تنشئ كائن إجراء وترسله إلى المتجر
-
٣
المتجر يرسل الإجراء إلى المخفض
المتجر يمرر الحالة الحالية والإجراء إلى وظيفة المخفض
-
٤
المخفض يحسب الحالة الجديدة
المخفض يعالج الإجراء ويعيد نسخة جديدة من الحالة (دون تعديل الحالة الأصلية)
-
٥
المتجر يحدث الحالة ويخطر المشتركين
المتجر يحفظ الحالة الجديدة ويخطر جميع المكونات المشتركة للتحديث
// مثال على تدفق البيانات في Redux
// 1. المستخدم ينقر على زر (يتفاعل مع التطبيق)
document.getElementById('addTodo').addEventListener('click', () => {
// 2. التطبيق يرسل إجراءً
store.dispatch({
type: 'ADD_TODO',
payload: { text: 'تعلم Redux', id: 1 }
});
// 3 & 4. المتجر يرسل الإجراء للمخفض الذي يحسب الحالة الجديدة (يحدث داخلياً)
// 5. المكونات المشتركة تتلقى الحالة المحدثة (عبر React-Redux)
});
// مخفض يعالج الإجراء (الخطوة 4)
function todosReducer(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
// إنشاء نسخة جديدة من الحالة مع العنصر الجديد
return [...state, action.payload];
default:
return state;
}
}
الخلاصة: المبادئ الثلاثة لـ Redux
- مصدر واحد للحقيقة: حالة التطبيق بأكملها مخزنة في شجرة كائن واحدة داخل متجر واحد.
- الحالة للقراءة فقط: الطريقة الوحيدة لتغيير الحالة هي إرسال إجراء - كائن يصف ما حدث.
- التغييرات تتم عبر دوال نقية: المخفضات هي دوال نقية تأخذ الحالة السابقة والإجراء، وتنتج الحالة التالية.
٣. إعداد مشروع Redux
التثبيت
لبدء استخدام Redux، نحتاج إلى تثبيت المكتبات الضرورية. سنبدأ بالمكتبات الأساسية، ثم نضيف المكتبات الإضافية حسب الحاجة.
تثبيت المكتبات الأساسية:
# باستخدام npm
npm install redux
# أو باستخدام yarn
yarn add redux
# لاستخدام Redux مع React، نضيف
npm install react-redux
# للمشاريع الجديدة، يوصى باستخدام Redux Toolkit
npm install @reduxjs/toolkit
السطر ١-٢: تثبيت مكتبة Redux الأساسية باستخدام npm.
السطر ٤-٥: تثبيت مكتبة Redux باستخدام yarn.
السطر ٧-٨: تثبيت react-redux للربط بين React و Redux.
السطر ١٠-١١: تثبيت Redux Toolkit وهو الطريقة الموصى بها حالياً لتطوير Redux.
نصيحة: للمشاريع الجديدة، نوصي باستخدام Redux Toolkit مباشرة بدلاً من Redux الأساسي، حيث يقدم أدوات وتسهيلات تبسط عملية التطوير.
إنشاء مشروع React مع Redux باستخدام Create React App:
# إنشاء مشروع React جديد مع قالب Redux
npx create-react-app my-app --template redux
# أو مع TypeScript
npx create-react-app my-app --template redux-typescript
السطر ١-٢: إنشاء مشروع React جديد باستخدام قالب Redux الرسمي.
السطر ٤-٥: إنشاء مشروع React مع Redux باستخدام TypeScript للحصول على تدقيق أنواع البيانات.
هيكل الملفات الموصى به
تنظيم ملفات مشروع Redux بشكل صحيح يساعد على الحفاظ على الكود منظماً وقابلاً للصيانة. فيما يلي هيكل ملفات موصى به:
src/
├── app/
│ ├── store.js # تكوين متجر Redux
│ └── hooks.js # hooks مخصصة (useSelector, useDispatch)
│
├── features/ # تقسيم حسب الميزات/الوظائف (feature-based)
│ ├── counter/
│ │ ├── counterSlice.js # slice تشمل المخفضات والإجراءات
│ │ ├── Counter.js # مكونات React المرتبطة
│ │ └── counterAPI.js # طلبات API (اختياري)
│ │
│ └── todos/
│ ├── todosSlice.js # slice لميزة المهام
│ ├── TodoList.js # مكونات القائمة
│ └── todosAPI.js # طلبات API
│
├── components/ # مكونات React المشتركة (قابلة لإعادة الاستخدام)
│
└── index.js # نقطة دخول التطبيق
app/store.js: تكوين المتجر الأساسي.
app/hooks.js: دوال hooks مخصصة تبسط استخدام useSelector و useDispatch.
features/: مجلد لتنظيم الكود حسب الوظائف/الميزات.
features/feature/featureSlice.js: مخفضات وإجراءات مرتبطة بميزة محددة.
components/: مكونات React المشتركة التي يمكن استخدامها في ميزات متعددة.
نهج تنظيم الملفات:
تنظيم حسب النوع (Type-Based)
src/ ├── actions/ │ ├── todoActions.js │ └── userActions.js ├── reducers/ │ ├── todoReducer.js │ └── userReducer.js └── components/ ├── TodoList.js └── UserProfile.js
مناسب للمشاريع الصغيرة أو للمبتدئين في Redux.
تنظيم حسب الميزة (Feature-Based)
src/ ├── features/ │ ├── todos/ │ │ ├── todosSlice.js │ │ ├── TodoList.js │ │ └── todosAPI.js │ └── users/ │ ├── usersSlice.js │ ├── UserProfile.js │ └── usersAPI.js
الطريقة الموصى بها لمشاريع متوسطة إلى كبيرة.
أفضل الممارسات: التنظيم حسب الميزة يجعل من السهل العثور على جميع الملفات المتعلقة بميزة محددة وفهم كيفية عملها معًا، مما يسهل إضافة ميزات جديدة أو تعديل الميزات الحالية.
٤. إنشاء المتجر (Store)
إنشاء المتجر الأساسي
المتجر هو القلب النابض لتطبيق Redux. إنه المكان الذي يتم فيه تخزين حالة التطبيق ومن خلاله يتم إرسال الإجراءات.
إنشاء متجر Redux بسيط:
// store.js
import { createStore } from 'redux';
import rootReducer from './reducers';
// إنشاء متجر Redux
const store = createStore(
rootReducer,
// وسيطة اختيارية: الحالة الأولية
// initialState,
// وسيطة اختيارية: محسنات المتجر (enhancers)
// window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
export default store;
السطر ٢: استيراد دالة createStore من Redux.
السطر ٣: استيراد المخفض الرئيسي (rootReducer) الذي يجمع كل المخفضات.
السطر ٦-١١: إنشاء متجر Redux مع ثلاث وسائط محتملة:
- الوسيطة ١: المخفض الرئيسي للتطبيق (إلزامي).
- الوسيطة ٢: الحالة الأولية (اختياري).
- الوسيطة ٣: محسنات المتجر مثل أدوات التطوير (اختياري).
السطر ١٣: تصدير المتجر ليتم استخدامه في التطبيق.
إنشاء متجر باستخدام Redux Toolkit (الطريقة الموصى بها):
// store.js
import { configureStore } from '@reduxjs/toolkit';
import todosReducer from '../features/todos/todosSlice';
import counterReducer from '../features/counter/counterSlice';
const store = configureStore({
reducer: {
todos: todosReducer,
counter: counterReducer,
},
// يمكن تكوين خيارات إضافية هنا
// middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(logger),
// devTools: process.env.NODE_ENV !== 'production',
});
export default store;
السطر ٢: استيراد configureStore من Redux Toolkit.
السطر ٣-٤: استيراد المخفضات من شرائح الميزات المختلفة.
السطر ٦-١٠: إنشاء المتجر مع تكوين كائن يحتوي على:
- reducer: كائن يحدد كيف سيتم تقسيم حالة التطبيق.
- السطر ١١-١٢: خيارات إضافية مثل middleware والأدوات التطويرية.
ميزات استخدام configureStore:
- تضمين Redux DevTools Extension تلقائياً
- إضافة middleware افتراضية تساعد في الكشف عن الأخطاء الشائعة
- تجميع المخفضات بشكل تلقائي
- تكوين أسهل وأقل تفصيلاً من createStore
العمليات الأساسية على المتجر
١. الوصول إلى الحالة
// الحصول على حالة المتجر الحالية
const state = store.getState();
تُستخدم للوصول إلى حالة التطبيق الحالية من أي مكان في التطبيق.
٢. إرسال الإجراءات
// إرسال إجراء لتحديث الحالة
store.dispatch({
type: 'ADD_TODO',
payload: { text: 'تعلم Redux', id: 1 }
});
الطريقة الوحيدة لتحديث الحالة في Redux. ترسل كائن إجراء إلى المخفضات.
٣. الاشتراك في التغييرات
// الاشتراك لتلقي إشعارات عند تغيير الحالة
const unsubscribe = store.subscribe(() => {
console.log('الحالة تغيرت!', store.getState());
});
// إلغاء الاشتراك عند الحاجة
unsubscribe();
تسمح بتنفيذ دالة معينة كلما تغيرت حالة المتجر.
٤. استبدال المخفض
// استبدال المخفض الحالي بمخفض جديد
import newRootReducer from './newReducers';
store.replaceReducer(newRootReducer);
مفيدة للتحميل المتأخر للمخفضات أو عند التحديث الساخن (hot reloading).
ملاحظة هامة: في تطبيقات React، نادراً ما نستخدم هذه الطرق مباشرة. بدلاً من ذلك، نستخدم واجهات React-Redux مثل useSelector و useDispatch للتفاعل مع المتجر.
مثال عملي: إنشاء وتكوين متجر Redux
لنلق نظرة على مثال كامل يوضح كيفية إنشاء متجر Redux في مشروع React باستخدام Redux Toolkit وربطه بالتطبيق:
١. إنشاء المخفض (مثال مخفض المهام):
// features/todos/todosSlice.js
import { createSlice } from '@reduxjs/toolkit';
const todosSlice = createSlice({
name: 'todos',
initialState: [],
reducers: {
addTodo: (state, action) => {
state.push(action.payload);
},
toggleTodo: (state, action) => {
const todo = state.find(todo => todo.id === action.payload);
if (todo) {
todo.completed = !todo.completed;
}
},
removeTodo: (state, action) => {
return state.filter(todo => todo.id !== action.payload);
}
}
});
export const { addTodo, toggleTodo, removeTodo } = todosSlice.actions;
export default todosSlice.reducer;
٢. إنشاء المتجر:
// app/store.js
import { configureStore } from '@reduxjs/toolkit';
import todosReducer from '../features/todos/todosSlice';
const store = configureStore({
reducer: {
todos: todosReducer
}
});
export default store;
٣. ربط المتجر بتطبيق React:
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import App from './App';
import store from './app/store';
ReactDOM.render(
,
document.getElementById('root')
);
٤. استخدام المتجر في مكون React:
// features/todos/TodoList.js
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { addTodo, toggleTodo, removeTodo } from './todosSlice';
function TodoList() {
const todos = useSelector(state => state.todos);
const dispatch = useDispatch();
const handleAddTodo = (text) => {
dispatch(addTodo({ id: Date.now(), text, completed: false }));
};
const handleToggle = (id) => {
dispatch(toggleTodo(id));
};
const handleRemove = (id) => {
dispatch(removeTodo(id));
};
return (
{/* واجهة المستخدم للقائمة */}
);
}
export default TodoList;
٥. الإجراءات (Actions)
ما هي الإجراءات؟
الإجراءات هي كائنات JavaScript تصف ما حدث في التطبيق. إنها الطريقة الوحيدة لإرسال البيانات من التطبيق إلى متجر Redux.
شكل الإجراء الأساسي:
{
type: 'ADD_TODO', // نوع الإجراء (إلزامي)
payload: { // البيانات (اختياري)
id: 1,
text: 'تعلم Redux',
completed: false
}
}
خصائص الإجراء:
- type: سلسلة نصية تصف نوع الإجراء (حقل إلزامي)
- payload: البيانات المرتبطة بالإجراء (اختياري)
- meta: معلومات إضافية غير متعلقة بالبيانات (اختياري)
- error: لتحديد ما إذا كان الإجراء يمثل خطأً (اختياري)
FSA (Flux Standard Action): اتفاقية شائعة لتوحيد بنية الإجراءات. وفقًا لـ FSA، يجب أن يكون الإجراء كائنًا عاديًا مع حقل type
إلزامي، وحقول اختيارية payload
، error
، وmeta
.
دوال موزعة الإجراءات (Action Creators)
دوال موزعة الإجراءات هي دوال تنشئ وتعيد إجراءات. تستخدم لتبسيط إنشاء الإجراءات وتوحيد بنيتها عبر التطبيق.
موزع إجراءات بسيط:
// actions/todoActions.js
// دالة موزعة لإضافة مهمة
export const addTodo = (text) => {
return {
type: 'ADD_TODO',
payload: {
id: Date.now(),
text,
completed: false
}
};
};
// دالة موزعة لتبديل حالة المهمة
export const toggleTodo = (id) => {
return {
type: 'TOGGLE_TODO',
payload: id
};
};
استخدام موزعات الإجراءات:
import { addTodo, toggleTodo } from './actions/todoActions';
import store from './store';
// إرسال إجراء إضافة مهمة
store.dispatch(addTodo('تعلم Redux'));
// إرسال إجراء تبديل حالة المهمة
store.dispatch(toggleTodo(1));
السطر ١: استيراد دوال موزعة الإجراءات.
السطر ٤-٥: استدعاء دالة addTodo وإرسال الإجراء الناتج.
السطر ٧-٨: استدعاء دالة toggleTodo وإرسال الإجراء الناتج.
فائدة دوال موزعة الإجراءات: تقلل من التكرار، وتبسط الاختبار، وتضمن اتساق شكل الإجراءات عبر التطبيق.
أنواع الإجراءات (Action Types)
أفضل الممارسات تقتضي تعريف أنواع الإجراءات كثوابت، مما يقلل من الأخطاء الإملائية ويسهل التتبع.
// constants/actionTypes.js
// أنواع إجراءات المهام
export const ADD_TODO = 'ADD_TODO';
export const TOGGLE_TODO = 'TOGGLE_TODO';
export const REMOVE_TODO = 'REMOVE_TODO';
// أنواع إجراءات الفلتر
export const SET_FILTER = 'SET_FILTER';
// استخدام الثوابت في موزعات الإجراءات
import { ADD_TODO, TOGGLE_TODO } from '../constants/actionTypes';
export const addTodo = (text) => {
return {
type: ADD_TODO,
payload: { id: Date.now(), text, completed: false }
};
};
export const toggleTodo = (id) => {
return {
type: TOGGLE_TODO,
payload: id
};
};
السطر ٤-٦: تعريف ثوابت لأنواع إجراءات المهام.
السطر ٩: تعريف ثابت لنوع إجراء الفلتر.
السطر ١٢: استيراد الثوابت لاستخدامها في موزعات الإجراءات.
السطر ١٤-١٩: استخدام الثوابت بدلاً من السلاسل النصية المباشرة.
الإجراءات غير المتزامنة
الإجراءات في Redux نقية وتزامنية بطبيعتها. للتعامل مع العمليات غير المتزامنة (مثل طلبات API)، نحتاج إلى middleware مثل redux-thunk أو redux-saga.
مثال على إجراء غير متزامن باستخدام Redux Thunk:
// actions/todoActions.js
import { ADD_TODO, TODOS_LOADING, TODOS_ERROR } from '../constants/actionTypes';
// إجراء غير متزامن لإضافة مهمة من خلال طلب API
export const addTodoAsync = (text) => {
// موزع الإجراءات يعيد دالة بدلاً من كائن إجراء
return async (dispatch) => {
// إرسال إجراء بدء التحميل
dispatch({ type: TODOS_LOADING });
try {
// طلب API لإضافة مهمة
const response = await fetch('/api/todos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ text })
});
if (!response.ok) {
throw new Error('فشل إضافة المهمة');
}
const newTodo = await response.json();
// إرسال إجراء نجاح إضافة المهمة
dispatch({
type: ADD_TODO,
payload: newTodo
});
} catch (error) {
// إرسال إجراء خطأ
dispatch({
type: TODOS_ERROR,
payload: error.message
});
}
};
};
السطر ٥-٣٣: دالة موزعة تعيد دالة أخرى (thunk) بدلاً من كائن إجراء.
السطر ٩: إرسال إجراء لتحديث حالة التحميل.
السطر ١٢-١٧: طلب API لإضافة مهمة جديدة.
السطر ٢٣-٢٦: إرسال إجراء نجاح إضافة المهمة عند نجاح الطلب.
السطر ٢٨-٣٢: إرسال إجراء خطأ عند فشل الطلب.
Redux Thunk
middleware بسيط يسمح بإعادة دوال من موزعات الإجراءات بدلاً من كائنات.
// تكوين Redux Thunk
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers';
const store = createStore(
rootReducer,
applyMiddleware(thunk)
);
مناسب للحالات البسيطة لإجراءات غير متزامنة.
معالجة الإجراءات في createSlice
في Redux Toolkit، يمكننا استخدام createAsyncThunk لتبسيط الإجراءات غير المتزامنة.
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
// إنشاء thunk غير متزامن
export const fetchTodos = createAsyncThunk(
'todos/fetchTodos',
async () => {
const response = await fetch('/api/todos');
return response.json();
}
);
const todosSlice = createSlice({
name: 'todos',
initialState: { items: [], loading: false, error: null },
reducers: { /* ... */ },
extraReducers: (builder) => {
builder
.addCase(fetchTodos.pending, (state) => {
state.loading = true;
})
.addCase(fetchTodos.fulfilled, (state, action) => {
state.loading = false;
state.items = action.payload;
})
.addCase(fetchTodos.rejected, (state, action) => {
state.loading = false;
state.error = action.error.message;
});
}
});
٦. المخفضات (Reducers)
ما هي المخفضات؟
المخفضات هي دوال نقية تحدد كيفية تغيير حالة التطبيق استجابةً للإجراءات. تأخذ الحالة السابقة وإجراءً كمدخلات، وتعيد حالة جديدة.
// بنية دالة المخفض الأساسية
function todoReducer(state = initialState, action) {
switch (action.type) {
case 'ADD_TODO':
return [...state, action.payload];
case 'TOGGLE_TODO':
return state.map(todo =>
todo.id === action.payload
? { ...todo, completed: !todo.completed }
: todo
);
case 'REMOVE_TODO':
return state.filter(todo => todo.id !== action.payload);
default:
return state; // مهم! يجب دائمًا إعادة الحالة الحالية لأنواع الإجراءات غير المعروفة
}
}
السطر ٢: تعريف دالة المخفض التي تأخذ الحالة السابقة وإجراء، وتعطي قيمة افتراضية للحالة.
السطر ٣: استخدام switch للتعامل مع أنواع الإجراءات المختلفة.
السطر ٤-٥: معالجة إجراء إضافة مهمة عبر إنشاء مصفوفة جديدة.
السطر ٦-١٠: معالجة إجراء تبديل حالة المهمة عبر استخدام map لإنشاء مصفوفة جديدة.
السطر ١١-١٢: معالجة إجراء حذف مهمة عبر تصفية المصفوفة.
السطر ١٣-١٤: الحالة الافتراضية التي تعيد الحالة بدون تغيير (مهمة جداً!).
القواعد الأساسية للمخفضات:
- يجب أن تكون المخفضات دوالاً نقية بدون آثار جانبية
- لا تعدل الحالة مباشرة، بل أنشئ دائمًا نسخة جديدة
- لا تستدعي طلبات API أو تغير بيانات خارج نطاق الدالة
- لا تستخدم دوالاً غير متوقعة النتائج مثل Date.now() أو Math.random()
- دائمًا أعد الحالة الحالية للإجراءات غير المعروفة
تجميع المخفضات
مع نمو التطبيق، من الأفضل تقسيم المخفضات إلى وحدات أصغر تتعامل مع أجزاء محددة من الحالة، ثم تجميعها معًا.
مخفضات منفصلة:
// reducers/todosReducer.js
export default function todosReducer(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
return [...state, action.payload];
case 'TOGGLE_TODO':
return state.map(todo =>
todo.id === action.payload
? { ...todo, completed: !todo.completed }
: todo
);
case 'REMOVE_TODO':
return state.filter(todo => todo.id !== action.payload);
default:
return state;
}
}
// reducers/filterReducer.js
export default function filterReducer(state = 'ALL', action) {
switch (action.type) {
case 'SET_FILTER':
return action.payload;
default:
return state;
}
}
تجميع المخفضات:
// reducers/index.js
import { combineReducers } from 'redux';
import todosReducer from './todosReducer';
import filterReducer from './filterReducer';
// تجميع المخفضات في مخفض رئيسي
const rootReducer = combineReducers({
todos: todosReducer,
filter: filterReducer
});
export default rootReducer;
السطر ٢: استيراد دالة combineReducers من Redux.
السطر ٣-٤: استيراد المخفضات الفرعية.
السطر ٧-١٠: تجميع المخفضات في مخفض رئيسي واحد.
النتيجة: سيكون شكل الحالة النهائي: { todos: [...], filter: 'ALL' }
تكوين المخفضات باستخدام Redux Toolkit:
// تجميع المخفضات تلقائيًا باستخدام configureStore
import { configureStore } from '@reduxjs/toolkit';
import todosReducer from '../features/todos/todosSlice';
import filterReducer from '../features/filter/filterSlice';
export const store = configureStore({
reducer: {
todos: todosReducer,
filter: filterReducer
}
});
باستخدام Redux Toolkit، تحل configureStore محل combineReducers وcreateStore، وتقوم بتجميع المخفضات تلقائيًا.
أنماط تحديث الحالة
تحديث الحالة بشكل غير متغير (immutable) هو مفتاح أداء Redux الجيد. إليك بعض الأنماط الشائعة لتحديث الحالة:
١. تحديث عناصر المصفوفة
// تحديث عنصر في مصفوفة
function updateArrayItem(array, itemId, updateFunction) {
return array.map(item => {
if (item.id !== itemId) {
// هذا ليس العنصر الذي نريد تحديثه - إعادته كما هو
return item;
}
// هذا هو العنصر الذي نريد تحديثه - إنشاء نسخة جديدة
return updateFunction(item);
});
}
// استخدام الدالة في المخفض
case 'UPDATE_TODO':
return updateArrayItem(
state,
action.payload.id,
todo => ({ ...todo, text: action.payload.text })
);
٢. حذف عنصر من مصفوفة
// حذف عنصر من مصفوفة
function removeItem(array, itemId) {
return array.filter(item => item.id !== itemId);
}
// استخدام الدالة في المخفض
case 'REMOVE_TODO':
return removeItem(state, action.payload);
٣. تحديث كائن متداخل
// تحديث كائن متداخل
case 'UPDATE_USER_INFO':
return {
...state,
user: {
...state.user,
info: {
...state.user.info,
[action.payload.field]: action.payload.value
}
}
};
٤. استخدام Immer في Redux Toolkit
// تبسيط التحديثات باستخدام Immer
import { createSlice } from '@reduxjs/toolkit';
const todosSlice = createSlice({
name: 'todos',
initialState: [],
reducers: {
// يمكن تعديل state مباشرة لأن Immer يتعامل مع عدم التغيير
addTodo: (state, action) => {
state.push(action.payload);
},
toggleTodo: (state, action) => {
const todo = state.find(todo => todo.id === action.payload);
if (todo) {
todo.completed = !todo.completed;
}
},
removeTodo: (state, action) => {
const index = state.findIndex(todo => todo.id === action.payload);
if (index !== -1) {
state.splice(index, 1);
}
}
}
});
نصيحة هامة: استخدم Redux Toolkit مع Immer المدمج به لتبسيط كتابة تحديثات الحالة غير المتغيرة. يمكنك كتابة كود "متغير" ظاهريًا، وسيقوم Immer بتحويله إلى تحديثات غير متغيرة تحت الغطاء.
٧. Redux مع React
ربط Redux مع React
لربط Redux مع React، نستخدم مكتبة React-Redux التي توفر واجهة متكاملة بين المكتبتين، وتتيح للمكونات الوصول إلى متجر Redux.
إعداد مزود Redux (Provider):
// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import App from './App';
import store from './app/store';
ReactDOM.render(
,
document.getElementById('root')
);
السطر ٤: استيراد مكون Provider من react-redux.
السطر ٨-١٠: تغليف التطبيق بأكمله بـ Provider وتمرير متجر Redux إليه.
الوظيفة: يجعل Provider متجر Redux متاحًا لجميع المكونات في شجرة المكونات.
استخدام Hooks للوصول إلى المتجر:
// TodoList.js
import React, { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { addTodo, toggleTodo, removeTodo } from './todosSlice';
function TodoList() {
const [text, setText] = useState('');
// استخدام useSelector للوصول إلى الحالة
const todos = useSelector(state => state.todos);
const filter = useSelector(state => state.filter);
// استخدام useDispatch لإرسال الإجراءات
const dispatch = useDispatch();
const handleSubmit = (e) => {
e.preventDefault();
if (!text.trim()) return;
dispatch(addTodo({
id: Date.now(),
text,
completed: false
}));
setText('');
};
const handleToggle = (id) => {
dispatch(toggleTodo(id));
};
const handleRemove = (id) => {
dispatch(removeTodo(id));
};
// تصفية المهام حسب الفلتر
const filteredTodos = todos.filter(todo => {
if (filter === 'ACTIVE') return !todo.completed;
if (filter === 'COMPLETED') return todo.completed;
return true; // ALL
});
return (
{filteredTodos.map(todo => (
-
handleToggle(todo.id)}
>
{todo.text}
))}
);
}
السطر ٣: استيراد hooks من react-redux: useSelector للوصول إلى الحالة و useDispatch لإرسال الإجراءات.
السطر ٩-١٠: استخدام useSelector لاختيار أجزاء محددة من حالة Redux.
السطر ١٣: الحصول على دالة dispatch لإرسال الإجراءات.
السطر ١٧-٢٤: إرسال إجراء addTodo عند تقديم النموذج.
السطر ٢٦-٣٢: دوال التعامل مع تبديل وحذف المهام.
السطر ٣٥-٣٩: تصفية المهام حسب الفلتر الحالي.
السطر ٤٢-٦٥: عرض واجهة المستخدم مع ربطها بحالة Redux.
Hooks الأساسية في React-Redux
useSelector
يتيح للمكونات استخراج البيانات من حالة Redux.
import { useSelector } from 'react-redux';
// اختيار قيمة واحدة
const counter = useSelector(state => state.counter);
// اختيار قيم متعددة
const { todos, filter } = useSelector(state => ({
todos: state.todos,
filter: state.filter
}));
// استخدام Reselect للحسابات المعقدة
import { createSelector } from '@reduxjs/toolkit';
const selectCompletedTodos = createSelector(
state => state.todos,
todos => todos.filter(todo => todo.completed)
);
const completedTodos = useSelector(selectCompletedTodos);
useDispatch
يعيد دالة dispatch التي تسمح بإرسال الإجراءات إلى متجر Redux.
import { useDispatch } from 'react-redux';
import { increment, decrement } from './counterSlice';
function Counter() {
const dispatch = useDispatch();
return (
{/* إرسال إجراء بوسائط */}
{/* إرسال إجراء غير متزامن */}
);
}
useStore
يعيد مرجعًا إلى نفس متجر Redux الذي تم تمريره إلى مكون Provider (نادر الاستخدام).
import { useStore } from 'react-redux';
function Component() {
const store = useStore();
// الحصول على الحالة الكاملة
const state = store.getState();
// استخدامات متقدمة
const unsubscribe = store.subscribe(() => {
console.log('تم تحديث الحالة!');
});
// لا تنسى إلغاء الاشتراك عند إزالة المكون
useEffect(() => {
return () => {
unsubscribe();
};
}, [unsubscribe]);
return (
// ...
);
}
أفضل الممارسات: استخدم useSelector لاختيار أجزاء صغيرة ومحددة من الحالة، وليس الحالة بأكملها. هذا يساعد في تحسين الأداء عن طريق تقليل عمليات إعادة العرض غير الضرورية.
إنشاء Hooks مخصصة
يمكن إنشاء hooks مخصصة لتبسيط استخدام Redux وتجنب تكرار نفس الكود في مكونات متعددة.
// app/hooks.js
import { useDispatch, useSelector } from 'react-redux';
import { TypedUseSelectorHook } from 'react-redux';
import type { RootState, AppDispatch } from './store';
// استخدام ملفات التعريف لـ TypeScript
export const useAppDispatch = () => useDispatch();
export const useAppSelector: TypedUseSelectorHook = useSelector;
// مثال على hook مخصص لتبسيط منطق المهام
export function useTodos() {
const todos = useAppSelector(state => state.todos);
const filter = useAppSelector(state => state.filter);
const dispatch = useAppDispatch();
// حساب المهام المصفاة
const filteredTodos = todos.filter(todo => {
if (filter === 'ACTIVE') return !todo.completed;
if (filter === 'COMPLETED') return todo.completed;
return true; // ALL
});
// منطق إضافة مهمة
const addNewTodo = (text) => {
dispatch(addTodo({
id: Date.now(),
text,
completed: false
}));
};
// منطق تبديل مهمة
const toggleTodoItem = (id) => {
dispatch(toggleTodo(id));
};
// منطق حذف مهمة
const removeTodoItem = (id) => {
dispatch(removeTodo(id));
};
return {
todos: filteredTodos,
filter,
addNewTodo,
toggleTodoItem,
removeTodoItem
};
}
استخدام Hook المخصص:
// components/TodoList.js
import React, { useState } from 'react';
import { useTodos } from '../app/hooks';
function TodoList() {
const [text, setText] = useState('');
const {
todos,
addNewTodo,
toggleTodoItem,
removeTodoItem
} = useTodos();
const handleSubmit = (e) => {
e.preventDefault();
if (!text.trim()) return;
addNewTodo(text);
setText('');
};
return (
{todos.map(todo => (
-
toggleTodoItem(todo.id)}
>
{todo.text}
))}
);
}
المزايا: تبسيط المكونات، وإعادة استخدام المنطق، وتسهيل الاختبار، وفصل منطق إدارة الحالة عن واجهة المستخدم.
٨. Redux Toolkit
مقدمة عن Redux Toolkit
Redux Toolkit هو المجموعة الرسمية المُوصى بها من الأدوات لتطوير Redux. تم تصميمه لتبسيط التجربة وتقليل الشفرة المتكررة وإنفاذ أفضل الممارسات.
المزايا الرئيسية:
- تقليل كمية الشفرة المطلوبة لكتابة تطبيقات Redux
- تبسيط تكوين متجر Redux مع إعداد أفضل الممارسات
- توفير طريقة آمنة لكتابة "تحديثات متغيرة" في المخفضات
- تضمين أدوات مفيدة للتعامل مع منطق غير متزامن
- إزالة الحاجة إلى مكتبات إضافية مثل Immer أو Redux Thunk
التثبيت:
# باستخدام npm
npm install @reduxjs/toolkit
# باستخدام yarn
yarn add @reduxjs/toolkit
# إنشاء مشروع جديد باستخدام Redux Toolkit
npx create-react-app my-app --template redux
createSlice - مفتاح Redux Toolkit
createSlice هي الأداة الرئيسية في Redux Toolkit. تقوم بإنشاء المخفضات وموزعات الإجراءات وأنواع الإجراءات في مكان واحد.
مثال لـ createSlice:
// features/counter/counterSlice.js
import { createSlice } from '@reduxjs/toolkit';
const initialState = {
value: 0,
status: 'idle'
};
export const counterSlice = createSlice({
name: 'counter', // اسم الشريحة (يستخدم لتوليد أنواع الإجراءات)
initialState, // الحالة الأولية
reducers: {
// تعريف المخفضات وموزعات الإجراءات معًا
increment: (state) => {
// يمكن "تعديل" الحالة مباشرة بفضل Immer
state.value += 1;
},
decrement: (state) => {
state.value -= 1;
},
// استخدام وسيطة في الإجراء
incrementByAmount: (state, action) => {
state.value += action.payload;
},
// استخدام دالة prepare لتحضير الوسائط
setStatusWithTimestamp: {
reducer: (state, action) => {
state.status = action.payload.status;
state.lastUpdated = action.payload.timestamp;
},
prepare: (status) => {
return {
payload: {
status,
timestamp: Date.now()
}
};
}
}
}
});
// تصدير موزعات الإجراءات المولدة تلقائيًا
export const {
increment,
decrement,
incrementByAmount,
setStatusWithTimestamp
} = counterSlice.actions;
// إنشاء دوال محددة (selectors) للوصول إلى الحالة
export const selectCount = (state) => state.counter.value;
export const selectStatus = (state) => state.counter.status;
// تصدير المخفض المولد تلقائيًا
export default counterSlice.reducer;
السطر ٢: استيراد createSlice من Redux Toolkit.
السطر ٤-٧: تعريف الحالة الأولية.
السطر ٩-٣٩: إنشاء slice باستخدام createSlice مع تحديد:
- name: اسم الشريحة (يستخدم لتوليد أنواع الإجراءات).
- initialState: الحالة الأولية.
- reducers: كائن يحتوي على دوال المخفض، كل مفتاح يصبح اسم إجراء.
السطر ١٥-٢٣: مخفضات بسيطة تستفيد من Immer للتعديل "المباشر" على الحالة.
السطر ٢٦-٣٧: مثال على تعريف معقد للمخفض مع دالة prepare.
السطر ٤٢-٤٧: تصدير موزعات الإجراءات المولدة تلقائيًا.
السطر ٥٠-٥١: تعريف دوال selector للوصول إلى الحالة.
السطر ٥٤: تصدير المخفض المولد.
مزايا استخدام createSlice:
- توليد موزعات الإجراءات تلقائيًا، مما يلغي الحاجة إلى كتابتها يدويًا
- توليد أنواع الإجراءات تلقائيًا
- دمج مكتبة Immer للتمكين من كتابة "تحديثات متغيرة" تُحول تلقائيًا إلى تحديثات غير متغيرة
- تنظيم المخفضات والإجراءات والحالة المرتبطة في مكان واحد (مفهوم "slice")
- تبسيط الكتابة وجعلها أكثر قابلية للقراءة والصيانة
createAsyncThunk - التعامل مع العمليات غير المتزامنة
تبسط createAsyncThunk طريقة التعامل مع العمليات غير المتزامنة مثل طلبات API في Redux. تولد هذه الأداة إجراءات البدء والنجاح والفشل تلقائيًا.
مثال للعمليات غير المتزامنة:
// features/users/usersSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
// إنشاء thunk غير متزامن لجلب بيانات المستخدمين
export const fetchUsers = createAsyncThunk(
'users/fetchUsers', // نوع الإجراء
async (_, { rejectWithValue }) => {
try {
const response = await fetch('https://api.example.com/users');
if (!response.ok) {
throw new Error('فشل جلب المستخدمين.');
}
const data = await response.json();
return data;
} catch (error) {
return rejectWithValue(error.message);
}
}
);
// إنشاء شريحة المستخدمين
const usersSlice = createSlice({
name: 'users',
initialState: {
entities: [],
status: 'idle', // 'idle' | 'loading' | 'succeeded' | 'failed'
error: null
},
reducers: {
// مخفضات عادية هنا
},
extraReducers: (builder) => {
// التعامل مع حالات الإجراءات غير المتزامنة
builder
.addCase(fetchUsers.pending, (state) => {
state.status = 'loading';
})
.addCase(fetchUsers.fulfilled, (state, action) => {
state.status = 'succeeded';
state.entities = action.payload;
})
.addCase(fetchUsers.rejected, (state, action) => {
state.status = 'failed';
state.error = action.payload || action.error.message;
});
}
});
export default usersSlice.reducer;
السطر ٢: استيراد createAsyncThunk من Redux Toolkit.
السطر ٥-١٩: إنشاء thunk غير متزامن لطلب API:
- الوسيطة ١: معرف نوع الإجراء ('users/fetchUsers').
- الوسيطة ٢: دالة تنفذ المنطق غير المتزامن وتعيد وعداً (Promise).
السطر ٢٩-٤٥: استخدام extraReducers للتعامل مع حالات الإجراء غير المتزامن:
- pending: عندما يبدأ الطلب (مثل تحديث حالة التحميل).
- fulfilled: عند نجاح الطلب (تحديث البيانات والحالة).
- rejected: عند فشل الطلب (تخزين رسالة الخطأ).
استخدام createAsyncThunk في المكونات:
// components/UsersList.js
import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { fetchUsers } from '../features/users/usersSlice';
function UsersList() {
const dispatch = useDispatch();
const users = useSelector(state => state.users.entities);
const status = useSelector(state => state.users.status);
const error = useSelector(state => state.users.error);
useEffect(() => {
// جلب البيانات عند تحميل المكون إذا لم يتم جلبها بعد
if (status === 'idle') {
dispatch(fetchUsers());
}
}, [status, dispatch]);
if (status === 'loading') {
return جاري التحميل...;
}
if (status === 'failed') {
return خطأ: {error};
}
return (
قائمة المستخدمين
{users.map(user => (
- {user.name}
))}
);
}
أدوات أخرى في Redux Toolkit
configureStore
تبسيط إنشاء متجر Redux مع إعدادات افتراضية معقولة.
import { configureStore } from '@reduxjs/toolkit';
import usersReducer from '../features/users/usersSlice';
import postsReducer from '../features/posts/postsSlice';
export const store = configureStore({
reducer: {
users: usersReducer,
posts: postsReducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware()
.concat(logger),
devTools: process.env.NODE_ENV !== 'production',
});
createEntityAdapter
أدوات لإدارة مجموعات من الكائنات ذات المعرفات في الحالة.
import { createEntityAdapter, createSlice } from '@reduxjs/toolkit';
// إنشاء entity adapter للمهام
const todosAdapter = createEntityAdapter({
// تحديد كيفية استخراج المعرف من كائن المهمة
selectId: (todo) => todo.id,
// ترتيب اختياري للكائنات
sortComparer: (a, b) => a.createdAt - b.createdAt,
});
// إنشاء الحالة الأولية
const initialState = todosAdapter.getInitialState({
status: 'idle',
error: null
});
const todosSlice = createSlice({
name: 'todos',
initialState,
reducers: {
todoAdded: todosAdapter.addOne,
todosReceived: todosAdapter.setAll,
todoUpdated: todosAdapter.updateOne,
todoRemoved: todosAdapter.removeOne,
}
});
// دوال للوصول إلى الحالة
export const {
selectAll: selectAllTodos,
selectById: selectTodoById,
selectIds: selectTodoIds
} = todosAdapter.getSelectors((state) => state.todos);
createSelector
إنشاء دوال اختيار مخزنة مؤقتاً للأداء العالي (من مكتبة Reselect).
import { createSelector } from '@reduxjs/toolkit';
// دوال اختيار أساسية
const selectUsers = state => state.users.entities;
const selectActiveFilter = state => state.filters.activeOnly;
// دالة اختيار مركبة
export const selectActiveUsers = createSelector(
[selectUsers, selectActiveFilter],
(users, activeOnly) => {
return activeOnly
? users.filter(user => user.isActive)
: users;
}
);
createReducer
إنشاء مخفض مباشرة بدلاً من استخدام createSlice (نادر الاستخدام).
import { createReducer, createAction } from '@reduxjs/toolkit';
// إنشاء الإجراءات
export const increment = createAction('counter/increment');
export const decrement = createAction('counter/decrement');
// إنشاء المخفض
const counterReducer = createReducer(0, (builder) => {
builder
.addCase(increment, (state, action) => {
return state + (action.payload || 1);
})
.addCase(decrement, (state, action) => {
return state - (action.payload || 1);
});
});
٩. التعامل مع الآثار الجانبية
مفهوم الآثار الجانبية في Redux
الآثار الجانبية هي أي تفاعل مع العالم خارج المخفض، مثل طلبات API، الوصول إلى التخزين المحلي، استخدام setTimeout، وغيرها. المخفضات في Redux دوال نقية لا تسمح بالآثار الجانبية، لذا نحتاج إلى طرق خاصة للتعامل معها.
أمثلة على الآثار الجانبية:
- طلبات HTTP (الجلب، التحميل، الحذف من خادم)
- الوصول إلى التخزين المحلي (localStorage)
- التفاعل مع قاعدة بيانات
- استخدام وظائف الزمن (setTimeout, setInterval)
- إنشاء أرقام عشوائية
- الحصول على الوقت الحالي
- تغييرات DOM المباشرة
- التفاعل مع APIs المتصفح
طرق التعامل مع الآثار الجانبية:
- Middleware: عناصر برمجية تعترض الإجراءات قبل وصولها للمخفضات
- Thunks: دوال تُرجع داخلها دوال أخرى للتعامل مع العمليات غير المتزامنة
- Sagas: نمذجة الآثار الجانبية باستخدام المولدات (generators)
- Observables: استخدام تدفقات RxJS للتعامل مع تسلسلات العمليات المعقدة
- Listeners: الاستماع للإجراءات والاستجابة لها بآثار جانبية
الفكرة الأساسية: المخفضات نفسها تبقى نقية وخالية من الآثار الجانبية، بينما يتم نقل الآثار الجانبية إلى أجزاء أخرى من التطبيق مثل middleware أو thunks أو sagas.
Redux Thunk - الطريقة الأبسط
Redux Thunk هو middleware يسمح بكتابة موزعات إجراءات تعيد دوالاً بدلاً من كائنات الإجراءات. الدوال المعادة (thunks) يمكنها تنفيذ آثار جانبية وإرسال إجراءات أخرى.
كيفية عمل Thunk:
// بدون thunk - موزع إجراء بسيط
const addTodo = (text) => {
return {
type: 'ADD_TODO',
payload: { text, id: Date.now() }
};
};
// مع thunk - موزع إجراء يعيد دالة
const fetchTodos = () => {
// تعيد دالة تأخذ dispatch وgetState كوسائط
return async (dispatch, getState) => {
// يمكن إرسال إجراءات لتحديث حالة التحميل
dispatch({ type: 'TODOS_LOADING' });
try {
// تنفيذ الأثر الجانبي (طلب API)
const response = await fetch('/api/todos');
const todos = await response.json();
// إرسال إجراء لتحديث الحالة بالبيانات المستلمة
dispatch({
type: 'TODOS_LOADED',
payload: todos
});
} catch (error) {
// إرسال إجراء لتحديث حالة الخطأ
dispatch({
type: 'TODOS_ERROR',
error: error.message
});
}
};
};
السطر ٢-٦: موزع إجراء عادي يعيد كائن إجراء.
السطر ٩-٣٢: موزع إجراء thunk يعيد دالة:
- السطر ١١: الدالة المعادة تأخذ dispatch و getState كوسائط.
- السطر ١٣: إرسال إجراء لتحديث حالة التحميل.
- السطر ١٧-١٨: تنفيذ طلب API (أثر جانبي).
- السطر ٢١-٢٤: عند النجاح، إرسال إجراء بالبيانات.
- السطر ٢٧-٣٠: عند الفشل، إرسال إجراء بالخطأ.
استخدام thunk في المكونات:
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { fetchTodos } from '../actions/todoActions';
function TodoList() {
const dispatch = useDispatch();
const { todos, loading, error } = useSelector(state => state.todos);
useEffect(() => {
// إرسال thunk لجلب البيانات
dispatch(fetchTodos());
}, [dispatch]);
if (loading) return جاري التحميل...;
if (error) return خطأ: {error};
return (
{todos.map(todo => (
- {todo.text}
))}
);
}
Redux-Saga - للآثار الجانبية المعقدة
Redux-Saga هي مكتبة خارجية تستخدم مولدات JavaScript (generators) للتعامل مع الآثار الجانبية المعقدة. توفر طرقًا أكثر تنظيمًا وقابلية للاختبار للتعامل مع تسلسلات العمليات المعقدة.
مثال بسيط على Saga:
import { takeLatest, call, put } from 'redux-saga/effects';
// دالة تنفذ طلب API
function fetchTodosApi() {
return fetch('/api/todos').then(res => res.json());
}
// saga لجلب المهام
function* fetchTodosSaga() {
try {
// استدعاء دالة API مع توقف (yield)
const todos = yield call(fetchTodosApi);
// إرسال إجراء نجاح مع البيانات
yield put({ type: 'TODOS_LOADED', payload: todos });
} catch (error) {
// إرسال إجراء خطأ
yield put({ type: 'TODOS_ERROR', error: error.message });
}
}
// saga رئيسية تراقب الإجراءات
export function* todosSaga() {
// تستجيب لأحدث إجراء من نوع FETCH_TODOS فقط
yield takeLatest('FETCH_TODOS', fetchTodosSaga);
}
السطر ١: استيراد الدوال الضرورية من redux-saga/effects.
السطر ٤-٦: دالة تنفذ طلب API.
السطر ٩-٢٠: saga مولد يتعامل مع جلب المهام:
- السطر ١٢: استدعاء دالة API وانتظار النتيجة باستخدام call.
- السطر ١٥: إرسال إجراء نجاح باستخدام put.
- السطر ١٨: إرسال إجراء خطأ عند الفشل.
السطر ٢٣-٢٦: saga رئيسية تراقب الإجراءات باستخدام takeLatest.
مميزات Redux Saga
- قوة في التعامل مع تسلسلات العمليات المعقدة
- التعامل مع السباقات (race conditions)
- إلغاء العمليات غير المتزامنة
- الاستجابة لتسلسلات الإجراءات
- قابلية اختبار أفضل
- تنفيذ منطق التزامن والإلغاء والتكرار
ميزات إضافية لـ Saga
// التعامل مع السباقات
function* fetchDataWithTimeout() {
yield race({
data: call(fetchData),
timeout: delay(5000)
});
}
// تنفيذ مهام متوازية
function* parallelTasks() {
yield all([
call(fetchUsers),
call(fetchPosts),
call(fetchComments)
]);
}
// إلغاء المهام
function* cancelableTask() {
const task = yield fork(longRunningTask);
yield take('CANCEL_TASK');
yield cancel(task);
}
RTK Query - الحل الشامل للتعامل مع البيانات
RTK Query هو إضافة قوية لـ Redux Toolkit تبسط جلب البيانات وتخزينها المؤقت وتحديثها والإدارة الكاملة لحالة البيانات الخارجية.
ميزات RTK Query:
- التخزين المؤقت التلقائي للبيانات مع استراتيجيات تجديد مرنة
- تجنب طلبات HTTP المتكررة للبيانات نفسها
- تتبع حالات التحميل لكل طلب
- إدارة الأخطاء والمحاولات التلقائية
- تحديثات متشائمة (optimistic updates) للبيانات
- إلغاء الطلبات تلقائيًا عند عدم الحاجة إليها بعد
// api/apiSlice.js
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
// إنشاء API بالكامل
export const apiSlice = createApi({
// اسم الشريحة وreducer المقابل
reducerPath: 'api',
// قاعدة الطلبات
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
// endpoints تمثل العمليات والطلبات
endpoints: (builder) => ({
// جلب البيانات (GET)
getTodos: builder.query({
query: () => '/todos',
// إعادة جلب البيانات كل 60 ثانية
pollingInterval: 60000,
// تحويل الاستجابة إذا لزم الأمر
transformResponse: (response) => response.sort((a, b) => b.id - a.id),
}),
// إرسال البيانات (POST)
addTodo: builder.mutation({
query: (todo) => ({
url: '/todos',
method: 'POST',
body: todo,
}),
// تحديث التخزين المؤقت بعد الإضافة
invalidatesTags: ['Todos'],
}),
// تحديث البيانات (PATCH)
updateTodo: builder.mutation({
query: ({ id, ...patch }) => ({
url: `/todos/${id}`,
method: 'PATCH',
body: patch,
}),
// تحديث متشائم للتخزين المؤقت
async onQueryStarted({ id, ...patch }, { dispatch, queryFulfilled }) {
const patchResult = dispatch(
apiSlice.util.updateQueryData('getTodos', undefined, (draft) => {
const todo = draft.find((todo) => todo.id === id);
if (todo) Object.assign(todo, patch);
})
);
try {
await queryFulfilled;
} catch {
patchResult.undo();
}
},
}),
}),
});
// تصدير hooks تلقائيًا
export const { useGetTodosQuery, useAddTodoMutation, useUpdateTodoMutation } = apiSlice;
السطر ٢: استيراد الدوال الضرورية من RTK Query.
السطر ٥-٥٣: إنشاء API slice مع:
- reducerPath: اسم الشريحة في متجر Redux.
- baseQuery: تكوين طريقة الاتصال بالخادم.
- endpoints: نقاط النهاية للطلبات (query للجلب، mutation للتعديل).
السطر ١٣-١٩: إنشاء query لجلب البيانات مع خيارات مثل الاستطلاع التلقائي.
السطر ٢٢-٢٩: إنشاء mutation لإضافة بيانات جديدة وإبطال التخزين المؤقت بعد الإضافة.
السطر ٣٢-٥٠: إنشاء mutation للتحديث مع تنفيذ تحديث متشائم للواجهة قبل اكتمال الطلب.
السطر ٥٥: تصدير hooks المُولّدة تلقائيًا لاستخدامها في المكونات.
استخدام RTK Query في المكونات:
import React from 'react';
import { useGetTodosQuery, useAddTodoMutation, useUpdateTodoMutation } from '../api/apiSlice';
function TodoList() {
// استخدام hooks من RTK Query
const {
data: todos,
isLoading,
isSuccess,
isError,
error,
refetch
} = useGetTodosQuery();
const [addTodo] = useAddTodoMutation();
const [updateTodo] = useUpdateTodoMutation();
const handleAddTodo = async (text) => {
try {
await addTodo({ text, completed: false });
} catch (err) {
console.error('فشل إضافة المهمة:', err);
}
};
const handleToggle = async (id, completed) => {
try {
await updateTodo({ id, completed: !completed });
} catch (err) {
console.error('فشل تحديث المهمة:', err);
}
};
// عرض حالات مختلفة
if (isLoading) return جاري التحميل...;
if (isError) return خطأ: {error.message};
return (
{todos.map(todo => (
-
handleToggle(todo.id, todo.completed)}
>
{todo.text}
))}
);
}
نصيحة: لتطبيقات جديدة تتطلب تفاعلاً كبيراً مع API، ينصح بشدة باستخدام RTK Query بدلاً من الحلول الأخرى. يوفر حلاً شاملاً لإدارة حالة البيانات الخارجية بأقل كمية من الشفرة.