تعلم Redux للمبتدئين: دليل عملي مبسّط لفهم Redux وRedux Toolkit
إذا كنت تبدأ رحلتك مع Redux وتشعر أن المصطلحات مثل store وreducers وactions تبدو معقدة، فهذا الدليل كُتب ليبسط الفكرة خطوة بخطوة. سنبني تطبيق قائمة مهام باستخدام React وRedux Toolkit، مع شرح عملي لكيفية إدارة الحالة محلياً ثم ربطها مع API.
في هذا المقال ستتعلم:
- ما مكونات
Reduxالأساسية، وكيف تتعاون فيما بينها. - كيفية جلب البيانات من
APIباستخدامRedux. - طريقة استخدام
Redux Toolkitلتقليل التعقيد وكتابة كود أوضح. - كيفية بناء تطبيق مهام يدعم الإضافة والحذف وتحديث حالة الإنجاز.

ما الذي سنبنيه في هذا الشرح؟
سنطوّر تطبيق Todo List بسيطاً من ناحية الواجهة، لكنه مهم جداً من ناحية الفهم العملي لإدارة الحالة. سيتمكن المستخدم من:
- إضافة مهمة جديدة.
- حذف مهمة.
- تحديد المهمة على أنها مكتملة.
- عرض عدد المهام المكتملة.

تجهيز المشروع وملفات البداية
للبدء، افتح الطرفية ونفّذ الأوامر التالية للحصول على ملفات المشروع وتشغيله:
git clone https://github.com/chrisblakely01/react-redux-todo-app.git
cd react-redux-todo-app/starter
npm install
npm start
بعد تشغيل المشروع، افتح مجلد react-redux-todo-app داخل بيئة التطوير التي تفضلها مثل VS Code.

ستجد عادةً المجلدات التالية:
api: يحتوي على واجهة برمجية سنستخدمها لاحقاً لتجربة العمل مع البيانات الخارجية.final: النسخة النهائية المكتملة من المشروع.starter: ملفات البداية التي سنعمل عليها أثناء الشرح.
نظرة سريعة على Redux: كيف تعمل إدارة الحالة؟
يتكوّن Redux من عدة أجزاء رئيسية، لكل جزء وظيفة محددة:
actions: أوامر تصف ما الذي حدث.reducers: دوال تستقبل الحالة الحالية والإجراء، ثم تُنتج حالة جديدة.state: البيانات الحالية للتطبيق.store: المكان المركزي الذي يحتفظ بالحالة كلها.

تخيّل أن لديك زرًا في الواجهة يضيف مبلغاً أو يضيف مهمة جديدة. عند النقر على الزر، يتم استدعاء دالة، ومن داخلها نستخدم dispatch لإرسال action. هذا الإجراء يحتوي غالباً على:
type: اسم الإجراء.payload: البيانات المرتبطة به، مثل عنوان المهمة أو معرفها.
بعد ذلك يستقبل store هذا الإجراء، ثم يمرره إلى reducer المناسب. وظيفة reducer هي قراءة الحالة الحالية وتحديثها بطريقة منطقية، ثم إعادة الحالة الجديدة. بمجرد حفظ الحالة الجديدة، تُحدّث المكونات التي تعتمد عليها تلقائياً.
لماذا نحتاج Redux من الأساس؟
في التطبيقات الصغيرة، قد يكون تمرير البيانات بين المكونات بسيطاً. لكن عند توسع المشروع وازدياد عدد المكونات، يصبح تتبع مصدر التغيير وكيفية انتقال البيانات أمراً مرهقاً. هنا تبرز قيمة Redux لأنه:
- يجعل البيانات في مكان مركزي واحد.
- يفصل المسؤوليات بين الواجهة والمنطق.
- يسهّل الاختبار والصيانة.
- يجعل سلوك التطبيق أكثر قابلية للتنبؤ.
إعداد المتجر باستخدام Redux Toolkit
سنستخدم Redux Toolkit لأنه الطريقة الحديثة والأسهل لتهيئة Redux. أنشئ مجلداً باسم redux داخل src، ثم أضف ملف store.js واكتب التالي:
import { configureStore } from '@reduxjs/toolkit';
export default configureStore({
reducer: {},
});
الدالة configureStore تختصر علينا كثيراً من الإعدادات اليدوية. فهي تنشئ store، وتجهّز دمج reducers، وتضيف middleware افتراضية مفيدة للعمل غير المتزامن لاحقاً.
ربط المتجر بالتطبيق
بعد إنشاء store، نحتاج إلى ربطه بالتطبيق حتى تتمكن المكونات من الوصول إلى الحالة. عدّل ملف index.js كما يلي:
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import store from './redux/store';
import { Provider } from 'react-redux';
ReactDOM.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>,
document.getElementById('root')
);
المكوّن Provider هو الجسر الذي يربط تطبيق React بمتجر Redux. بمجرد تغليف التطبيق به، تصبح المكونات الداخلية قادرة على قراءة الحالة وإرسال الإجراءات.
إنشاء Slice لإدارة المهام
في Redux Toolkit نستخدم مفهوم slice، وهو طريقة منظمة لتجميع الحالة والمنطق المرتبط بها داخل ملف واحد. أنشئ ملف todoSlice.js داخل src/redux:
import { createSlice } from '@reduxjs/toolkit';
export const todoSlice = createSlice({
name: 'todos',
initialState: [
{ id: 1, title: 'todo1', completed: false },
{ id: 2, title: 'todo2', completed: false },
{ id: 3, title: 'todo3', completed: true },
{ id: 4, title: 'todo4', completed: false },
{ id: 5, title: 'todo5', completed: false },
],
reducers: {
addTodo: (state, action) => {
const todo = {
id: new Date(),
title: action.payload.title,
completed: false,
};
state.push(todo);
},
},
});
export const { addTodo } = todoSlice.actions;
export default todoSlice.reducer;
ما الذي يحدث هنا؟
name: اسم هذا الجزء من الحالة.initialState: البيانات الافتراضية الأولية.reducers: الدوال التي تعدّل الحالة.
لاحظ أن createSlice ينشئ لك actions تلقائياً بناءً على أسماء reducers. لذلك عندما كتبنا addTodo داخل reducers، حصلنا تلقائياً على إجراء باسم addTodo.
إضافة الـ Reducer إلى المتجر
الآن يجب تسجيل reducer الخاص بالمهام داخل المتجر:
import { configureStore } from '@reduxjs/toolkit';
import todoReducer from './todoSlice';
export default configureStore({
reducer: {
todos: todoReducer,
},
});
بهذه الطريقة يصبح جزء الحالة الخاص بالمهام متاحاً داخل المسار state.todos.
إضافة مهمة جديدة باستخدام dispatch
أنشأنا الإجراء والمنطق، لكن لن يحدث شيء حتى نرسل هذا الإجراء من الواجهة. داخل ملف AddTodoForm.js استخدم useDispatch لإرسال الإجراء عند إرسال النموذج:
import React, { useState } from 'react';
import { useDispatch } from 'react-redux';
import { addTodo } from '../redux/todoSlice';
const AddTodoForm = () => {
const [value, setValue] = useState('');
const dispatch = useDispatch();
const onSubmit = (event) => {
event.preventDefault();
if (value) {
dispatch(
addTodo({
title: value,
})
);
}
};
return (
<form onSubmit={onSubmit} className='form-inline mt-3 mb-3'>
<label className='sr-only'>Name</label>
<input
type='text'
className='form-control mb-2 mr-sm-2'
placeholder='Add todo...'
value={value}
onChange={(event) => setValue(event.target.value)}
></input>
<button type='submit' className='btn btn-primary mb-2'>
Submit
</button>
</form>
);
};
export default AddTodoForm;
الدالة useDispatch تمنحك إمكانية إرسال الإجراءات إلى Redux. وعند استدعاء dispatch(addTodo(...))، سيتم تحديث الحالة في المتجر.

عرض قائمة المهام باستخدام useSelector
بعد تحديث الحالة، نحتاج إلى قراءة البيانات من Redux وعرضها داخل الواجهة. هنا يأتي دور useSelector:
import React from 'react';
import TodoItem from './TodoItem';
import { useSelector } from 'react-redux';
const TodoList = () => {
const todos = useSelector((state) => state.todos);
return (
<ul className='list-group'>
{todos.map((todo) => (
<TodoItem
id={todo.id}
title={todo.title}
completed={todo.completed}
/>
))}
</ul>
);
};
export default TodoList;
تستقبل useSelector دالة تُمرّر إليها شجرة الحالة كاملة، ثم تعيد فقط الجزء الذي تحتاجه. في حالتنا نريد state.todos.
تحديث حالة المهمة إلى مكتملة
إضافة reducer جديد
للسماح للمستخدم بوضع علامة الإنجاز على المهمة، أضف toggleComplete داخل todoSlice.js:
import { createSlice } from '@reduxjs/toolkit';
export const todoSlice = createSlice({
name: 'todos',
initialState: [
{ id: 1, title: 'todo1', completed: false },
{ id: 2, title: 'todo2', completed: false },
{ id: 3, title: 'todo3', completed: true },
{ id: 4, title: 'todo4', completed: false },
{ id: 5, title: 'todo5', completed: false },
],
reducers: {
addTodo: (state, action) => {
const todo = {
id: new Date(),
title: action.payload.title,
completed: false,
};
state.push(todo);
},
toggleComplete: (state, action) => {
const index = state.findIndex((todo) => todo.id === action.payload.id);
state[index].completed = action.payload.completed;
},
},
});
export const { addTodo, toggleComplete } = todoSlice.actions;
export default todoSlice.reducer;
يعتمد هذا المنطق على id لتحديد المهمة المستهدفة، ثم يغيّر قيمة completed بناءً على البيانات القادمة من payload.
إرسال الإجراء من العنصر
import React from 'react';
import { useDispatch } from 'react-redux';
import { toggleComplete } from '../redux/todoSlice';
const TodoItem = ({ id, title, completed }) => {
const dispatch = useDispatch();
const handleCheckboxClick = () => {
dispatch(toggleComplete({ id, completed: !completed }));
};
return (
<li className={`list-group-item ${completed && 'list-group-item-success'}`}>
<div className='d-flex justify-content-between'>
<span className='d-flex align-items-center'>
<input
type='checkbox'
className='mr-3'
onClick={handleCheckboxClick}
checked={completed}
></input>
{title}
</span>
<button className='btn btn-danger'>Delete</button>
</div>
</li>
);
};
export default TodoItem;
حذف مهمة من القائمة
إضافة منطق الحذف
import { createSlice } from '@reduxjs/toolkit';
export const todoSlice = createSlice({
name: 'todos',
initialState: [
{ id: 1, title: 'todo1', completed: false },
{ id: 2, title: 'todo2', completed: false },
{ id: 3, title: 'todo3', completed: true },
{ id: 4, title: 'todo4', completed: false },
{ id: 5, title: 'todo5', completed: false },
],
reducers: {
addTodo: (state, action) => {
const todo = {
id: new Date(),
title: action.payload.title,
completed: false,
};
state.push(todo);
},
toggleComplete: (state, action) => {
const index = state.findIndex((todo) => todo.id === action.payload.id);
state[index].completed = action.payload.completed;
},
deleteTodo: (state, action) => {
return state.filter((todo) => todo.id !== action.payload.id);
},
},
});
export const { addTodo, toggleComplete, deleteTodo } = todoSlice.actions;
export default todoSlice.reducer;
هنا استخدمنا الدالة filter() لإرجاع مصفوفة جديدة لا تحتوي على المهمة المحذوفة.
إرسال إجراء الحذف
import React from 'react';
import { useDispatch } from 'react-redux';
import { toggleComplete, deleteTodo } from '../redux/todoSlice';
const TodoItem = ({ id, title, completed }) => {
const dispatch = useDispatch();
const handleCheckboxClick = () => {
dispatch(toggleComplete({ id, completed: !completed }));
};
const handleDeleteClick = () => {
dispatch(deleteTodo({ id }));
};
return (
<li className={`list-group-item ${completed && 'list-group-item-success'}`}>
<div className='d-flex justify-content-between'>
<span className='d-flex align-items-center'>
<input
type='checkbox'
className='mr-3'
onClick={handleCheckboxClick}
checked={completed}
></input>
{title}
</span>
<button onClick={handleDeleteClick} className='btn btn-danger'>
Delete
</button>
</div>
</li>
);
};
export default TodoItem;
عرض عدد المهام المكتملة
يمكننا بسهولة حساب عدد العناصر المكتملة عبر useSelector مع filter():
import React from 'react';
import { useSelector } from 'react-redux';
const TotalCompleteItems = () => {
const todos = useSelector((state) =>
state.todos.filter((todo) => todo.completed === true)
);
return <h4 className='mt-3'>Total complete items: {todos.length}</h4>;
};
export default TotalCompleteItems;
هذا الأسلوب مفيد لأنه يوضح كيف يمكن قراءة جزء محدد فقط من الحالة، أو حتى تطبيق تحويلات وعمليات تصفية مباشرة داخل selector.
كيف يعمل Redux مع API؟
عند التعامل مع البيانات الخارجية، لا يكفي استخدام reducers التقليدية وحدها، لأن reducer يجب أن يبقى دالة نقية. لذلك نستخدم middleware، وغالباً من خلال thunk.

الفكرة ببساطة:
- المكوّن يرسل إجراءً غير متزامن.
- تعترضه
middleware. - يتم تنفيذ طلب
API. - عند اكتمال الطلب، يُرسل إجراء عادي إلى
reducer. - تتحدث الحالة وتُعاد عملية التصيير.
تشغيل API المحلية
لتجربة الاتصال بـ API المرفقة، شغّلها بالأوامر التالية:
git clone https://github.com/chrisblakely01/react-redux-todo-app.git
cd react-redux-todo-app/api
npm install
npm run server
ثم افتح الرابط localhost:7000/todos داخل المتصفح للتأكد من أن البيانات تعمل كما ينبغي.

جلب المهام من API باستخدام createAsyncThunk
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
export const getTodosAsync = createAsyncThunk(
'todos/getTodosAsync',
async () => {
const resp = await fetch('http://localhost:7000/todos');
if (resp.ok) {
const todos = await resp.json();
return { todos };
}
}
);
export const todoSlice = createSlice({
name: 'todos',
initialState: [
{ id: 1, title: 'todo1', completed: false },
{ id: 2, title: 'todo2', completed: false },
{ id: 3, title: 'todo3', completed: true },
{ id: 4, title: 'todo4', completed: false },
{ id: 5, title: 'todo5', completed: false },
],
reducers: {
addTodo: (state, action) => {
const todo = {
id: new Date(),
title: action.payload.title,
completed: false,
};
state.push(todo);
},
toggleComplete: (state, action) => {
const index = state.findIndex((todo) => todo.id === action.payload.id);
state[index].completed = action.payload.completed;
},
deleteTodo: (state, action) => {
return state.filter((todo) => todo.id !== action.payload.id);
},
},
extraReducers: {
[getTodosAsync.fulfilled]: (state, action) => {
return action.payload.todos;
},
},
});
export const { addTodo, toggleComplete, deleteTodo } = todoSlice.actions;
export default todoSlice.reducer;
الدالة createAsyncThunk تنشئ إجراء غير متزامن يمر بمراحل مثل pending وfulfilled وrejected. في هذا المثال تعاملنا مع الحالة fulfilled لأننا نريد حفظ البيانات بعد نجاح الطلب.
إرسال طلب الجلب عند تحميل القائمة
import React, { useEffect } from 'react';
import TodoItem from './TodoItem';
import { useSelector, useDispatch } from 'react-redux';
import { getTodosAsync } from '../redux/todoSlice';
const TodoList = () => {
const dispatch = useDispatch();
const todos = useSelector((state) => state.todos);
useEffect(() => {
dispatch(getTodosAsync());
}, [dispatch]);
return (
<ul className='list-group'>
{todos.map((todo) => (
<TodoItem id={todo.id} title={todo.title} completed={todo.completed} />
))}
</ul>
);
};
export default TodoList;
إضافة مهمة جديدة عبر API
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
export const getTodosAsync = createAsyncThunk(
'todos/getTodosAsync',
async () => {
const resp = await fetch('http://localhost:7000/todos');
if (resp.ok) {
const todos = await resp.json();
return { todos };
}
}
);
export const addTodoAsync = createAsyncThunk(
'todos/addTodoAsync',
async (payload) => {
const resp = await fetch('http://localhost:7000/todos', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ title: payload.title }),
});
if (resp.ok) {
const todo = await resp.json();
return { todo };
}
}
);
export const todoSlice = createSlice({
name: 'todos',
initialState: [],
reducers: {
addTodo: (state, action) => {
const todo = {
id: new Date(),
title: action.payload.title,
completed: false,
};
state.push(todo);
},
toggleComplete: (state, action) => {
const index = state.findIndex((todo) => todo.id === action.payload.id);
state[index].completed = action.payload.completed;
},
deleteTodo: (state, action) => {
return state.filter((todo) => todo.id !== action.payload.id);
},
},
extraReducers: {
[getTodosAsync.fulfilled]: (state, action) => {
return action.payload.todos;
},
[addTodoAsync.fulfilled]: (state, action) => {
state.push(action.payload.todo);
},
},
});
export const { addTodo, toggleComplete, deleteTodo } = todoSlice.actions;
export default todoSlice.reducer;
ثم عدّل نموذج الإضافة لاستخدام addTodoAsync بدلاً من addTodo:
import React, { useState } from 'react';
import { useDispatch } from 'react-redux';
import { addTodoAsync } from '../redux/todoSlice';
const AddTodoForm = () => {
const [value, setValue] = useState('');
const dispatch = useDispatch();
const onSubmit = (event) => {
event.preventDefault();
if (value) {
dispatch(
addTodoAsync({
title: value,
})
);
}
};
return (
<form onSubmit={onSubmit} className='form-inline mt-3 mb-3'>
<label className='sr-only'>Name</label>
<input
type='text'
className='form-control mb-2 mr-sm-2'
placeholder='Add todo...'
value={value}
onChange={(event) => setValue(event.target.value)}
></input>
<button type='submit' className='btn btn-primary mb-2'>
Submit
</button>
</form>
);
};
export default AddTodoForm;
الميزة هنا أن المهمة لا تُضاف فقط إلى الواجهة، بل تُحفَظ أيضاً في المصدر الخارجي، لذلك تبقى موجودة بعد تحديث الصفحة.
تحديث حالة الإنجاز عبر API
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
export const getTodosAsync = createAsyncThunk(
'todos/getTodosAsync',
async () => {
const resp = await fetch('http://localhost:7000/todos');
if (resp.ok) {
const todos = await resp.json();
return { todos };
}
}
);
export const addTodoAsync = createAsyncThunk(
'todos/addTodoAsync',
async (payload) => {
const resp = await fetch('http://localhost:7000/todos', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ title: payload.title }),
});
if (resp.ok) {
const todo = await resp.json();
return { todo };
}
}
);
export const toggleCompleteAsync = createAsyncThunk(
'todos/completeTodoAsync',
async (payload) => {
const resp = await fetch(`http://localhost:7000/todos/${payload.id}`, {
method: 'PATCH',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ completed: payload.completed }),
});
if (resp.ok) {
const todo = await resp.json();
return { todo };
}
}
);
export const todoSlice = createSlice({
name: 'todos',
initialState: [],
reducers: {
addTodo: (state, action) => {
const todo = {
id: new Date(),
title: action.payload.title,
completed: false,
};
state.push(todo);
},
toggleComplete: (state, action) => {
const index = state.findIndex((todo) => todo.id === action.payload.id);
state[index].completed = action.payload.completed;
},
deleteTodo: (state, action) => {
return state.filter((todo) => todo.id !== action.payload.id);
},
},
extraReducers: {
[getTodosAsync.fulfilled]: (state, action) => {
return action.payload.todos;
},
[addTodoAsync.fulfilled]: (state, action) => {
state.push(action.payload.todo);
},
[toggleCompleteAsync.fulfilled]: (state, action) => {
const index = state.findIndex((todo) => todo.id === action.payload.todo.id);
state[index].completed = action.payload.todo.completed;
},
},
});
export const { addTodo, toggleComplete, deleteTodo } = todoSlice.actions;
export default todoSlice.reducer;
ثم حدّث مكوّن العنصر لإرسال toggleCompleteAsync:
import React from 'react';
import { useDispatch } from 'react-redux';
import { toggleCompleteAsync, deleteTodo } from '../redux/todoSlice';
const TodoItem = ({ id, title, completed }) => {
const dispatch = useDispatch();
const handleCheckboxClick = () => {
dispatch(toggleCompleteAsync({ id, completed: !completed }));
};
const handleDeleteClick = () => {
dispatch(deleteTodo({ id }));
};
return (
<li className={`list-group-item ${completed && 'list-group-item-success'}`}>
<div className='d-flex justify-content-between'>
<span className='d-flex align-items-center'>
<input
type='checkbox'
className='mr-3'
onClick={handleCheckboxClick}
checked={completed}
></input>
{title}
</span>
<button onClick={handleDeleteClick} className='btn btn-danger'>
Delete
</button>
</div>
</li>
);
};
export default TodoItem;
استخدمنا طلب PATCH لأننا نحدّث جزءاً من الكائن فقط، وليس الكائن كله.
تحدٍّ عملي: حذف مهمة عبر API
إذا أردت ترسيخ الفهم، فجرّب تنفيذ حذف المهمة من API بنفسك. الفكرة العامة ستكون كالتالي:
- إنشاء
async thunkجديد للحذف. - تنفيذ طلب
DELETEباستخدامfetch(). - إضافة معالج داخل
extraReducersلتحديث الحالة بعد نجاح الحذف. - إرسال الإجراء من زر الحذف في المكوّن.
شكل الطلب سيكون قريباً من هذا:
const resp = await fetch(`http://localhost:7000/todos/${payload.id}`, {
method: 'DELETE',
});
أفضل ممارسات عند تعلم Redux
- ابدأ بفهم تدفق البيانات قبل حفظ الأوامر.
- استخدم
Redux Toolkitبدلاً من الإعداد التقليدي متى أمكن. - اجعل
reducersبسيطة وواضحة ومركزة على وظيفة واحدة. - افصل منطق
APIداخلthunksأو ملفات منظمة. - استعن بأدوات
Redux DevToolsلفهم ما يحدث خطوة بخطوة.

الخلاصة التقنية
إذا أردت رأياً عملياً، فإن Redux يصبح مفيداً فعلاً عندما تتجاوز إدارة الحالة حدود البساطة داخل التطبيق. أما Redux Toolkit فقد جعل استخدامه أكثر واقعية وأقل إزعاجاً من السابق، خصوصاً مع createSlice وcreateAsyncThunk. أفضل طريقة لتعلّم Redux ليست قراءة التعاريف فقط، بل بناء مشروع صغير مثل تطبيق المهام، ثم تطويره تدريجياً ليشمل عمليات القراءة والإضافة والتحديث والحذف من API. عندها ستفهم المنظومة كاملة بصورة طبيعية وواضحة.