طَبِّقْهاتعلّم المبدأ، وطبّقه بيدكمقرر 242 · أطر الويب
الدرس 7 · الوحدة 2: إتقان إدارة الحالة

Redux Toolkit: مخزن واحد للتطبيق

حالة التطبيق الكبير في مكان واحد يمكن تتبعه. ⏱ نحو 35 دقيقة.

بعد هذا الدرس ستستطيع أن:
  • تشرح متى تحتاج Redux، وتقارنه بـ Context و Vuex.
  • تنشئ شريحة بـ createSlice ومخزنًا بـ configureStore.
  • تقرأ الحالة بـ useSelector وترسل الأفعال بـ useDispatch.
  • تكتب محدِّدات للقيم المحسوبة، وتفهم دور Immer.

١لماذا Redux؟

في التطبيقات الكبيرة (متجر، أو نظام إدارة) تتشارك عشرات المكوّنات حالة كثيرة: المستخدم، والسلة، والطلبات، والإشعارات. Redux يضع حالة التطبيق كلها في مخزن Store واحد خارج المكوّنات، ويغيّرها بالأسلوب الذي تعلمته في الدرس السابق: أفعال تُرسل إلى مخفّضات.

واليوم يُكتب Redux بـ Redux Toolkit (RTK)، الطريقة الرسمية الموصى بها، لأنه يختصر الكود المكرر. وتذكر الخطة أيضًا Vuex: الفكرة نفسها في عالم Vue (وخلفه اليوم Pinia).

Context + useReducerRedux Toolkit
الحجم المناسبتطبيق صغير أو متوسطتطبيق كبير بحالة كثيرة متشابكة
أدوات المطورمحدودةRedux DevTools: ترى كل فعل وتعود بالزمن
الأداءكل من يقرأ السياق يُعاد رسمهالمكوّن يُعاد رسمه فقط إن تغيّر ما يختاره
التعلّمجزء من Reactمكتبة إضافية بمفاهيم جديدة

٢القطع الأربع

القطعةالدالةوظيفتها
الشريحة SlicecreateSliceجزء من الحالة (السلة) مع مخفّضاته، وتنشئ الأفعال تلقائيًّا.
المخزن StoreconfigureStoreيجمع الشرائح في حالة واحدة.
المزوّد<Provider store={store}>يجعل المخزن متاحًا لكل المكوّنات.
الخطّافاتuseSelector و useDispatchالمكوّن يختار ما يحتاجه من الحالة، ويرسل الأفعال.

افتح الملفات الخمسة بالترتيب: الشريحة ثم المخزن ثم App ثم المكوّنان:

import { Provider } from 'react-redux';import { store } from './store.js';import Header from './Header.jsx';import Menu from './Menu.jsx'; export default function App() {  return (    <Provider store={store}>      <Header />      <Menu />    </Provider>  );}

٣«تعديل» الحالة داخل الشريحة؟

لاحظت في cartSlice: found.qty++ و state.items.push(…). ألم نقل لا تعدّل الحالة؟ داخل createSlice فقط يجوز ذلك، لأن RTK يستخدم مكتبة اسمها Immer: تكتب كأنك تعدّل، وهي تنتج نسخة جديدة فعليًّا في الخلفية.

خارج createSliceداخل مخفّضات createSlice
إضافة[...items, x]state.items.push(x) ✓
تعديلitems.map(…{...i, qty: i.qty + 1}…)found.qty++ ✓
استبدال الكلreturn { items: [] }state.items = [] ✓ (أو return)

والفعل الذي تنشئه الشريحة كائن عادي؛ القيمة التي تمررها تصل في action.payload:

import { added, removed } from './cartSlice.js'; const a = added({ id: 7, name: 'شاي', price: 18 });console.log(a.type);console.log(JSON.stringify(a.payload));console.log(removed(7).type, removed(7).payload); export default function App() {  return <p>الأفعال في Console.</p>;}

٤القيم المحسوبة: المحدِّدات

لا تخزّن ما يمكن حسابه (الإجمالي، وعدد العناصر). اكتب دوال صغيرة تختار وتحسب من الحالة، اسمها المحدِّدات Selectors، وضعها مع الشريحة لتستخدمها المكوّنات كلها:

// في cartSlice.jsexport const selectItems = state => state.cart.items;export const selectCount = state => state.cart.items.reduce((s, i) => s + i.qty, 0);export const selectTotal = state => state.cart.items.reduce((s, i) => s + i.price * i.qty, 0); // في أي مكوّنconst total = useSelector(selectTotal);

وبعد أن نبني الخادم، تأتي بيانات المنتجات من API. يقدّم RTK لذلك أداتين: createAsyncThunk لطلبات غير متزامنة، و RTK Query لجلب البيانات وتخزينها مؤقتًا. سنعود لهما في الوحدة 7.

توقّع وتحقّق

أين تُحفظ حالة التطبيق في Redux؟

Single source of truth.

القيمة الممررة إلى مُنشئ الفعل تصل في:

payload.

لماذا يجوز state.items.push داخل createSlice؟

Immer.

ليقرأ المكوّن جزءًا من المخزن يستخدم:

يختار ما يحتاجه.

الخلاصة

تحدٍّ برمجي: شريحة الإشعارات

متقدم

أضف إلى التطبيق شريحة إشعارات بـ Redux Toolkit.

المطلوب:
  • شريحة notes حالتها الأولى []، ومخفّضاها: pushed (يضيف نصًّا في payload) و dismissed (يحذف الإشعار بترتيبه index في payload).
  • المخزن فيه الشريحة بالاسم notes.
  • زر بالمعرّف id="new" يرسل pushed بنص «إشعار جديد»، وكل إشعار <li> فيه زر «✕» يرسل dismissed.
  • عنوان <h2>الإشعارات (2)</h2> بالعدد عبر useSelector.
👀 اعرض الناتج المطلوب (وجرّبه)
✓ التحقق الآلي من حلّك:
  • الكود يعمل بلا أخطاء في Console
  • في البداية: الإشعارات (0)
  • ثلاث ضغطات على #new تضيف ثلاثة
  • «✕» يحذف إشعارًا
  • تستخدم createSlice و configureStore و Provider
  • تستخدم useSelector و useDispatch

💡 تلميح 1
داخل مخفّضات الشريحة يجوز state.push(…) و state.splice(i, 1).
💡 تلميح 2
غلّف المكوّن الذي يستخدم useSelector بـ Provider في App.

حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.

import { configureStore, createSlice } from '@reduxjs/toolkit';import { Provider, useSelector, useDispatch } from 'react-redux'; const notes = createSlice({  name: 'notes',  initialState: [],  reducers: {    pushed(state, action) { state.push(action.payload); },    dismissed(state, action) { state.splice(action.payload, 1); },  },});const { pushed, dismissed } = notes.actions;const store = configureStore({ reducer: { notes: notes.reducer } }); function Notes() {  const list = useSelector(s => s.notes);  const dispatch = useDispatch();  return (    <>      <h2>الإشعارات ({list.length})</h2>      <button id="new" onClick={() => dispatch(pushed('إشعار جديد'))}>جديد</button>      <ul>        {list.map((n, i) => (          <li key={i}>{n} <button onClick={() => dispatch(dismissed(i))}>✕</button></li>        ))}      </ul>    </>  );} export default function App() {  return <Provider store={store}><Notes /></Provider>;}
انتقل إلى تقييم الدرساختيار من متعدد، وصح وخطأ، وأكمل الفراغ، وتتبّع الناتج.ابدأ التقييم ←

📝 معلم؟ أرسل ملاحظة للمؤلف عن هذا الدرس