مثال: جلب قائمة مقالات (Posts) من API باستخدام Axios + Redux Toolkit
ونعرِض:
- حالة التحميل (Loading)
- حالة النجاح (Success)
- حالة الخطأ (Error)
سأقسّم الشرح لمراحل:
- تركيب المكتبات
- عمل ملف Axios
- إنشاء slice فيها
createAsyncThunk - إعداد الـ store
- عمل كمبوننت تعرض البيانات وتتفاعل مع الحالات
1️⃣ تركيب المكتبات المطلوبة
لو ما ركّبتها من قبل:
npm install @reduxjs/toolkit react-redux axiosJavaScript@reduxjs/toolkit→ Redux Toolkitreact-redux→ لربط React مع Reduxaxios→ للطلبات HTTP
2️⃣ عمل ملف Axios مستقل (اختياري لكنه أفضل تنظيمًا)
ملف: api.js
import axios from 'axios';
// نعمل instance جاهز من axios
const api = axios.create({
baseURL: 'https://jsonplaceholder.typicode.com', // مثال API تجريبي
// ممكن تضيف headers ثابتة هنا لو احتجت
});
export default api;
JavaScriptالفائدة: لو بكرا غيرت الـ baseURL أو أضفت توكن، تعدّل مكان واحد فقط.
3️⃣ إنشاء Slice للتعامل مع الـ API
ملف: postsSlice.js
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
import api from './api';
// 1) الحالة الابتدائية للـ posts
const initialState = {
items: [], // هنا نخزن البيانات (المقالات)
status: 'idle', // idle | loading | succeeded | failed
error: null, // نخزن رسالة الخطأ لو حصل خطأ
};
// 2) إنشاء thunk لجلب المقالات من الـ API
// createAsyncThunk ترجع action async جاهزة فيها 3 حالات: pending, fulfilled, rejected
export const fetchPosts = createAsyncThunk(
'posts/fetchPosts', // اسم الـ action type
async (_, thunkAPI) => { // أول باراميتر: البيانات المرسلة، ثاني باراميتر: thunkAPI (اختياري)
try {
const response = await api.get('/posts'); // GET https://jsonplaceholder.typicode.com/posts
// لازم نرجع data فقط
return response.data; // هذا يسكن في action.payload في حالة fulfilled
} catch (error) {
// نرمي خطأ منسّق للـ rejected
return thunkAPI.rejectWithValue(error.message || 'حدث خطأ غير متوقع');
}
}
);
// 3) إنشاء slice
const postsSlice = createSlice({
name: 'posts',
initialState,
reducers: {
// هنا لو عندك أكشنات عادية (sync) تضيفها
},
extraReducers: (builder) => {
// ✅ حالة الطلب بدأ (pending)
builder.addCase(fetchPosts.pending, (state) => {
state.status = 'loading'; // نغيّر الحالة إلى "جاري التحميل"
state.error = null; // نفضي الخطأ لو كان موجود من قبل
});
// ✅ حالة الطلب نجح (fulfilled)
builder.addCase(fetchPosts.fulfilled, (state, action) => {
state.status = 'succeeded'; // نجح
state.items = action.payload; // نحفظ البيانات القادمة من الـ API
});
// ✅ حالة الطلب فشل (rejected)
builder.addCase(fetchPosts.rejected, (state, action) => {
state.status = 'failed'; // فشل
// لو استخدمنا rejectWithValue نأخذ القيمة من action.payload
state.error = action.payload || action.error.message || 'خطأ ما';
});
},
});
// 4) نصدر الـ reducer
export default postsSlice.reducer;
JavaScript🔍 شرح مهم لكل حالة:
fetchPosts.pending- يتم استدعاؤها تلقائيًا عندما تستدعي
dispatch(fetchPosts())وقبل ما يخلص الطلب. - نستخدمها عادةً لـ:
- وضع
status = 'loading' - إظهار سبينر / نص “جاري التحميل”
- وضع
- يتم استدعاؤها تلقائيًا عندما تستدعي
fetchPosts.fulfilled- تُستدعى عندما ينجح الـ API call بدون خطأ.
- القيمة المرجعة من الـ thunk (
return response.data) تكون في:action.payload - هنا نحدّث:
status = 'succeeded'items = action.payload
fetchPosts.rejected- تُستدعى عندما يحصل خطأ (network, 500, 404, … أو throw).
- إذا استخدمت
rejectWithValueفالرسالة اللي ترجعها بتكون داخلaction.payload. - نحدّث:
status = 'failed'error = رسالة الخطأ
4️⃣ إعداد الـ Store
ملف: store.js
import { configureStore } from '@reduxjs/toolkit';
import postsReducer from './postsSlice';
export const store = configureStore({
reducer: {
posts: postsReducer, // state.posts → يتحكم فيه postsReducer
},
});
JavaScript5️⃣ ربط الـ store مع React
ملف: main.jsx أو index.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(
<Provider store={store}>
<App />
</Provider>
);
JavaScript6️⃣ كمبوننت تعرض البيانات + الحالات
ملف: PostsList.jsx
import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { fetchPosts } from './postsSlice';
function PostsList() {
const dispatch = useDispatch();
// نقرأ من الـ state:
const posts = useSelector((state) => state.posts.items);
const status = useSelector((state) => state.posts.status);
const error = useSelector((state) => state.posts.error);
// نطلب البيانات أول مرة فقط
useEffect(() => {
if (status === 'idle') {
// أول مرة، ما طلبنا شيء قبل → نبدأ الطلب
dispatch(fetchPosts());
}
}, [status, dispatch]);
// 🔄 1) حالة التحميل
if (status === 'loading') {
return <p>جاري تحميل المقالات...</p>;
}
// ✅ 2) حالة النجاح
if (status === 'succeeded') {
return (
<div>
<h2>قائمة المقالات</h2>
<ul>
{posts.map((post) => (
<li key={post.id}>
<strong>{post.title}</strong>
<p>{post.body}</p>
</li>
))}
</ul>
</div>
);
}
// ❌ 3) حالة الخطأ
if (status === 'failed') {
return <p style={{ color: 'red' }}>حدث خطأ: {error}</p>;
}
// ⏳ حالة idle (قبل أي طلب)
return <button onClick={() => dispatch(fetchPosts())}>تحميل المقالات</button>;
}
export default PostsList;
JavaScriptما الذي يحدث هنا بالتسلسل؟
- أول ما يُرندر الكمبوننت:
statusفي البدايةidleحسبinitialState.
- في
useEffect:if (status === 'idle') dispatch(fetchPosts());- يُرسل
fetchPosts→ يبدأ الطلب.
- يُرسل
- Redux Toolkit تلقائيًا:
- يرسل action من النوع:
posts/fetchPosts/pending - يمر على
extraReducers→ يشغّل:builder.addCase(fetchPosts.pending, ...) - فتتغير
statusإلى'loading'.
- يرسل action من النوع:
- الكمبوننت يعيد الرندر:
- يرى
status === 'loading' - يعرض:
"جاري تحميل المقالات..."
- يرى
- عندما ينجح الطلب:
- Toolkit ترسل action ثانية:
posts/fetchPosts/fulfilled - تفعّل:
builder.addCase(fetchPosts.fulfilled, ...) - نضع البيانات في
state.itemsونغيرstatusإلى'succeeded'.
- Toolkit ترسل action ثانية:
- الكمبوننت يعيد الرندر مرة أخرى:
- يرى
status === 'succeeded' - يعرض
posts.map(...)على الشاشة.
- يرى
- لو حصل خطأ في الطلب:
- Toolkit ترسل:
posts/fetchPosts/rejected - تفعّل:
builder.addCase(fetchPosts.rejected, ...) - نضع
status = 'failed'وerror = message - الكمبوننت يعرض رسالة الخطأ.
- Toolkit ترسل:
7️⃣ شكل الـ actions في الخلفية (فهم أعمق شوي)
لما تستدعي:
dispatch(fetchPosts());JavaScriptRedux Toolkit تنفّذ هذا التسلسل:
- ترسل action:
{ type: 'posts/fetchPosts/pending' } - بعد ما يخلص الـ async:
- لو نجح:
{ type: 'posts/fetchPosts/fulfilled', payload: response.data } - لو فشل:
{ type: 'posts/fetchPosts/rejected', payload: 'الرسالة اللي رجّعتها في rejectWithValue' (لو استخدمت) // أو error: { message: '...' } لو ما استخدمت rejectWithValue }
- لو نجح:
عشان كذا في extraReducers نستخدم:
fetchPosts.pendingfetchPosts.fulfilledfetchPosts.rejected
بدل ما نكتب string type يدويًا.
🧠 خلاصة عامة
- Axios مسؤول عن الاتصال بالـ API فقط.
- createAsyncThunk يغلّف منطق الـ async ويعطيك 3 حالات تلقائيًا.
- extraReducers مكان التعامل مع هذه الحالات وتحديث
stateحسب النتيجة. - في الكمبوننت:
- نستخدم
useEffectلطلب البيانات. - نقرأ
status,items,error. - نعرض UI مختلف لكل حالة (loading / success / error / idle).
- نستخدم
لو حاب بعد كذا نطوّر المثال إلى:
- إضافة post جديد باستخدام
POST - أو حذف / تعديل
أقدر أبني لك نفس الفكرة معcreateAsyncThunkثاني وشرح كامل.