مقدمة إلى Redux Toolkit
Redux Toolkit (RTK) هو مجموعة الأدوات الرسمية والموجهة والشاملة لتطوير Redux بكفاءة. يبسط إعداد Redux، ويقلل من الكود الزائد، ويتضمن أفضل الممارسات بشكل افتراضي. RTK هو الآن الطريقة الموصى بها لكتابة منطق Redux.
لماذا Redux Toolkit؟
Redux التقليدي يتطلب كودًا زائدًا كبيرًا. Redux Toolkit يحل المشاكل الشائعة:
فوائد Redux Toolkit:
- إعداد مخزن مبسط مع ()configureStore
- تكامل تلقائي مع Redux DevTools
- Immer مدمج للتحديثات غير القابلة للتغيير
- ()createSlice يقلل الكود الزائد بنسبة 70%
- يتضمن createAsyncThunk للمنطق غير المتزامن
- دعم TypeScript جاهز
التثبيت والإعداد
ثبت Redux Toolkit و React-Redux:
# استخدام npm
npm install @reduxjs/toolkit react-redux
# استخدام yarn
yarn add @reduxjs/toolkit react-redux
# هيكل المشروع
src/
├── store/
│ ├── index.js # إعداد المخزن
│ ├── slices/
│ │ ├── counterSlice.js # ميزة العداد
│ │ ├── todoSlice.js # ميزة المهام
│ │ └── userSlice.js # ميزة المستخدم
│ └── hooks.js # Hooks مكتوبة
├── App.jsx
└── main.jsx
إنشاء أول مخزن
أعد مخزن Redux مع configureStore:
// store/index.js
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from './slices/counterSlice';
import todoReducer from './slices/todoSlice';
import userReducer from './slices/userSlice';
export const store = configureStore({
reducer: {
counter: counterReducer,
todos: todoReducer,
user: userReducer
},
// DevTools مفعل تلقائيًا في التطوير
// Middleware يتضمن thunk افتراضيًا
});
// استنتاج الأنواع من المخزن
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
// main.jsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import { Provider } from 'react-redux';
import { store } from './store';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
إنشاء الشرائح مع createSlice
الشريحة تمثل منطق Redux للميزة. createSlice ينشئ الإجراءات والـ reducers تلقائيًا:
// store/slices/counterSlice.js
import { createSlice } from '@reduxjs/toolkit';
const initialState = {
value: 0,
history: [],
step: 1
};
export const counterSlice = createSlice({
name: 'counter',
initialState,
reducers: {
// Redux Toolkit يستخدم Immer، يمكنك "تعديل" الحالة مباشرة
increment: (state) => {
state.value += state.step;
state.history.push({ action: 'increment', value: state.value });
},
decrement: (state) => {
state.value -= state.step;
state.history.push({ action: 'decrement', value: state.value });
},
// الإجراءات يمكن أن تقبل payloads
incrementByAmount: (state, action) => {
state.value += action.payload;
state.history.push({
action: 'incrementByAmount',
value: state.value,
amount: action.payload
});
},
setStep: (state, action) => {
state.step = action.payload;
},
reset: (state) => {
// يمكن إرجاع حالة جديدة أو التعديل
return initialState;
},
clearHistory: (state) => {
state.history = [];
}
}
});
// منشئو الإجراءات يتم إنشاؤهم تلقائيًا
export const {
increment,
decrement,
incrementByAmount,
setStep,
reset,
clearHistory
} = counterSlice.actions;
// تصدير الـ reducer
export default counterSlice.reducer;
// المحددات
export const selectCount = (state) => state.counter.value;
export const selectStep = (state) => state.counter.step;
export const selectHistory = (state) => state.counter.history;
سحر Immer: Redux Toolkit يستخدم مكتبة Immer داخليًا، والتي تسمح لك بكتابة كود "متغير" ينتج فعليًا تحديثات غير قابلة للتغيير. يمكنك إما تعديل حالة المسودة أو إرجاع حالة جديدة، ولكن ليس كلاهما في نفس الـ reducer.
استخدام Redux في المكونات
الوصول إلى الحالة مع useSelector وإرسال الإجراءات مع useDispatch:
// components/Counter.jsx
import { useSelector, useDispatch } from 'react-redux';
import {
increment,
decrement,
incrementByAmount,
setStep,
reset,
selectCount,
selectStep,
selectHistory
} from '../store/slices/counterSlice';
function Counter() {
const count = useSelector(selectCount);
const step = useSelector(selectStep);
const history = useSelector(selectHistory);
const dispatch = useDispatch();
const handleIncrement = () => {
dispatch(increment());
};
const handleDecrement = () => {
dispatch(decrement());
};
const handleIncrementByAmount = () => {
const amount = parseInt(prompt('أدخل المبلغ:'), 10);
if (!isNaN(amount)) {
dispatch(incrementByAmount(amount));
}
};
const handleStepChange = (e) => {
dispatch(setStep(parseInt(e.target.value, 10)));
};
const handleReset = () => {
dispatch(reset());
};
return (
<div className="counter">
<h2>العداد: {count}</h2>
<div className="controls">
<button onClick={handleDecrement}>-{step}</button>
<button onClick={handleIncrement}>+{step}</button>
<button onClick={handleIncrementByAmount}>
إضافة مبلغ مخصص
</button>
<button onClick={handleReset}>إعادة تعيين</button>
</div>
<div className="step-control">
<label>
حجم الخطوة:
<input
type="number"
value={step}
onChange={handleStepChange}
min="1"
/>
</label>
</div>
{history.length > 0 && (
<div className="history">
<h3>السجل</h3>
<ul>
{history.map((entry, index) => (
<li key={index}>
{entry.action}: {entry.value}
{entry.amount && ` (${entry.amount})`}
</li>
))}
</ul>
</div>
)}
</div>
);
}
قائمة المهام مع Redux Toolkit
مثال أكثر تعقيدًا مع الفلترة وإجراءات متعددة:
// store/slices/todoSlice.js
import { createSlice } from '@reduxjs/toolkit';
const initialState = {
items: [],
filter: 'all', // all, active, completed
nextId: 1
};
export const todoSlice = createSlice({
name: 'todos',
initialState,
reducers: {
addTodo: (state, action) => {
state.items.push({
id: state.nextId++,
text: action.payload,
completed: false,
createdAt: Date.now()
});
},
toggleTodo: (state, action) => {
const todo = state.items.find(item => item.id === action.payload);
if (todo) {
todo.completed = !todo.completed;
}
},
deleteTodo: (state, action) => {
state.items = state.items.filter(item => item.id !== action.payload);
},
editTodo: (state, action) => {
const { id, text } = action.payload;
const todo = state.items.find(item => item.id === id);
if (todo) {
todo.text = text;
}
},
setFilter: (state, action) => {
state.filter = action.payload;
},
clearCompleted: (state) => {
state.items = state.items.filter(item => !item.completed);
},
toggleAll: (state) => {
const allCompleted = state.items.every(item => item.completed);
state.items.forEach(item => {
item.completed = !allCompleted;
});
}
}
});
export const {
addTodo,
toggleTodo,
deleteTodo,
editTodo,
setFilter,
clearCompleted,
toggleAll
} = todoSlice.actions;
export default todoSlice.reducer;
// المحددات
export const selectAllTodos = (state) => state.todos.items;
export const selectFilter = (state) => state.todos.filter;
export const selectFilteredTodos = (state) => {
const { items, filter } = state.todos;
switch (filter) {
case 'active':
return items.filter(item => !item.completed);
case 'completed':
return items.filter(item => item.completed);
default:
return items;
}
};
export const selectTodoStats = (state) => {
const items = state.todos.items;
return {
total: items.length,
active: items.filter(item => !item.completed).length,
completed: items.filter(item => item.completed).length
};
};
أداء المحددات: المحددات المعقدة مثل selectFilteredTodos تعمل في كل تصيير. للحسابات المكلفة، استخدم مكتبة reselect (مضمنة مع RTK) لإنشاء محددات محفوظة في الذاكرة تعيد الحساب فقط عندما تتغير مدخلاتها.
إنشاء Hooks مكتوبة
أنشئ إصدارات مكتوبة من useDispatch و useSelector لدعم TypeScript أفضل:
// store/hooks.js
import { useDispatch, useSelector } from 'react-redux';
// استخدم في جميع أنحاء تطبيقك بدلاً من useDispatch و useSelector العادية
export const useAppDispatch = () => useDispatch();
export const useAppSelector = useSelector;
// الاستخدام في المكونات
import { useAppDispatch, useAppSelector } from '../store/hooks';
function MyComponent() {
const dispatch = useAppDispatch();
const count = useAppSelector((state) => state.counter.value);
// ...
}
استدعاء Prepare للإجراءات المعقدة
استخدم استدعاء prepare عندما تحتاج لتخصيص payloads الإجراءات:
// store/slices/postSlice.js
import { createSlice, nanoid } from '@reduxjs/toolkit';
export const postSlice = createSlice({
name: 'posts',
initialState: [],
reducers: {
addPost: {
reducer: (state, action) => {
state.push(action.payload);
},
// استدعاء Prepare يعمل قبل الـ reducer
prepare: (title, content, userId) => {
return {
payload: {
id: nanoid(), // توليد معرف فريد
title,
content,
userId,
createdAt: new Date().toISOString(),
reactions: { thumbsUp: 0, heart: 0 }
}
};
}
},
addReaction: (state, action) => {
const { postId, reaction } = action.payload;
const post = state.find(post => post.id === postId);
if (post) {
post.reactions[reaction]++;
}
}
}
});
// الاستخدام
dispatch(addPost('عنواني', 'محتوى المنشور', userId));
// يحصل تلقائيًا على id و createdAt و reactions
تمرين عملي 1: سلة تسوق مع Redux Toolkit
أنشئ مخزن سلة تسوق:
- أنشئ cartSlice مع مصفوفة عناصر، الإجمالي، والخصم
- نفذ إجراءات: addItem، removeItem، updateQuantity، applyDiscount، clearCart
- أنشئ محددات: selectCartItems، selectCartTotal، selectItemCount
- ابنِ مكون Cart يعرض العناصر مع عناصر تحكم الكمية
- أضف CartSummary يعرض الإجمالي والخصم
تمرين عملي 2: حالة مصادقة المستخدم
ابنِ شريحة auth:
- أنشئ authSlice مع user، token، isAuthenticated، isLoading
- أضف إجراءات: loginStart، loginSuccess، loginFailure، logout
- أنشئ محددات: selectUser، selectIsAuthenticated، selectAuthError
- ابنِ مكون LoginForm يرسل إجراءات تسجيل الدخول
- أنشئ مكون ProtectedRoute يفحص حالة المصادقة
تمرين عملي 3: نظام الإشعارات
أنشئ شريحة إدارة الإشعارات:
- ابنِ notificationSlice مع مصفوفة إشعارات
- نفذ addNotification (مع ID تلقائي)، removeNotification، clearAll
- استخدم استدعاء prepare لإضافة timestamp و type (success/error/warning/info)
- أنشئ مكون NotificationList يعرض جميع الإشعارات
- أضف الإغلاق التلقائي بعد 5 ثوانٍ باستخدام setTimeout في المكون
الخلاصة
Redux Toolkit يبسط تطوير Redux بشكل كبير. configureStore يعد المخزن بإعدادات جيدة افتراضيًا، createSlice يزيل الكود الزائد، وImmer يجعل التحديثات غير القابلة للتغيير سهلة. استخدم hooks مكتوبة لدعم TypeScript أفضل، واستفد من المحددات للوصول الفعال للحالة. RTK هو الطريقة الحديثة الموصى بها لاستخدام Redux.