أساسيات React.js

مقدمة إلى Redux Toolkit

20 دقيقة الدرس 22 من 40

مقدمة إلى 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

أنشئ مخزن سلة تسوق:

  1. أنشئ cartSlice مع مصفوفة عناصر، الإجمالي، والخصم
  2. نفذ إجراءات: addItem، removeItem، updateQuantity، applyDiscount، clearCart
  3. أنشئ محددات: selectCartItems، selectCartTotal، selectItemCount
  4. ابنِ مكون Cart يعرض العناصر مع عناصر تحكم الكمية
  5. أضف CartSummary يعرض الإجمالي والخصم

تمرين عملي 2: حالة مصادقة المستخدم

ابنِ شريحة auth:

  1. أنشئ authSlice مع user، token، isAuthenticated، isLoading
  2. أضف إجراءات: loginStart، loginSuccess، loginFailure، logout
  3. أنشئ محددات: selectUser، selectIsAuthenticated، selectAuthError
  4. ابنِ مكون LoginForm يرسل إجراءات تسجيل الدخول
  5. أنشئ مكون ProtectedRoute يفحص حالة المصادقة

تمرين عملي 3: نظام الإشعارات

أنشئ شريحة إدارة الإشعارات:

  1. ابنِ notificationSlice مع مصفوفة إشعارات
  2. نفذ addNotification (مع ID تلقائي)، removeNotification، clearAll
  3. استخدم استدعاء prepare لإضافة timestamp و type (success/error/warning/info)
  4. أنشئ مكون NotificationList يعرض جميع الإشعارات
  5. أضف الإغلاق التلقائي بعد 5 ثوانٍ باستخدام setTimeout في المكون

الخلاصة

Redux Toolkit يبسط تطوير Redux بشكل كبير. configureStore يعد المخزن بإعدادات جيدة افتراضيًا، createSlice يزيل الكود الزائد، وImmer يجعل التحديثات غير القابلة للتغيير سهلة. استخدم hooks مكتوبة لدعم TypeScript أفضل، واستفد من المحددات للوصول الفعال للحالة. RTK هو الطريقة الحديثة الموصى بها لاستخدام Redux.