createAsyncThunk

مثال: جلب قائمة مقالات (Posts) من API باستخدام Axios + Redux Toolkit
ونعرِض:

  • حالة التحميل (Loading)
  • حالة النجاح (Success)
  • حالة الخطأ (Error)

سأقسّم الشرح لمراحل:

  1. تركيب المكتبات
  2. عمل ملف Axios
  3. إنشاء slice فيها createAsyncThunk
  4. إعداد الـ store
  5. عمل كمبوننت تعرض البيانات وتتفاعل مع الحالات

1️⃣ تركيب المكتبات المطلوبة

لو ما ركّبتها من قبل:

npm install @reduxjs/toolkit react-redux axios
JavaScript
  • @reduxjs/toolkit → Redux Toolkit
  • react-redux → لربط React مع Redux
  • axios → للطلبات 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

🔍 شرح مهم لكل حالة:

  1. fetchPosts.pending
    • يتم استدعاؤها تلقائيًا عندما تستدعي dispatch(fetchPosts()) وقبل ما يخلص الطلب.
    • نستخدمها عادةً لـ:
      • وضع status = 'loading'
      • إظهار سبينر / نص “جاري التحميل”
  2. fetchPosts.fulfilled
    • تُستدعى عندما ينجح الـ API call بدون خطأ.
    • القيمة المرجعة من الـ thunk (return response.data) تكون في: action.payload
    • هنا نحدّث:
      • status = 'succeeded'
      • items = action.payload
  3. 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
  },
});
JavaScript
5️⃣ ربط الـ 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>
);
JavaScript
6️⃣ كمبوننت تعرض البيانات + الحالات

ملف: 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

ما الذي يحدث هنا بالتسلسل؟

  1. أول ما يُرندر الكمبوننت:
    • status في البداية idle حسب initialState.
  2. في useEffect: if (status === 'idle') dispatch(fetchPosts());
    • يُرسل fetchPosts → يبدأ الطلب.
  3. Redux Toolkit تلقائيًا:
    • يرسل action من النوع: posts/fetchPosts/pending
    • يمر على extraReducers → يشغّل: builder.addCase(fetchPosts.pending, ...)
    • فتتغير status إلى 'loading'.
  4. الكمبوننت يعيد الرندر:
    • يرى status === 'loading'
    • يعرض: "جاري تحميل المقالات..."
  5. عندما ينجح الطلب:
    • Toolkit ترسل action ثانية: posts/fetchPosts/fulfilled
    • تفعّل: builder.addCase(fetchPosts.fulfilled, ...)
    • نضع البيانات في state.items ونغير status إلى 'succeeded'.
  6. الكمبوننت يعيد الرندر مرة أخرى:
    • يرى status === 'succeeded'
    • يعرض posts.map(...) على الشاشة.
  7. لو حصل خطأ في الطلب:
    • Toolkit ترسل: posts/fetchPosts/rejected
    • تفعّل: builder.addCase(fetchPosts.rejected, ...)
    • نضع status = 'failed' و error = message
    • الكمبوننت يعرض رسالة الخطأ.

7️⃣ شكل الـ actions في الخلفية (فهم أعمق شوي)

لما تستدعي:

dispatch(fetchPosts());
JavaScript

Redux Toolkit تنفّذ هذا التسلسل:

  1. ترسل action: { type: 'posts/fetchPosts/pending' }
  2. بعد ما يخلص الـ async:
    • لو نجح: { type: 'posts/fetchPosts/fulfilled', payload: response.data }
    • لو فشل: { type: 'posts/fetchPosts/rejected', payload: 'الرسالة اللي رجّعتها في rejectWithValue' (لو استخدمت) // أو error: { message: '...' } لو ما استخدمت rejectWithValue }

عشان كذا في extraReducers نستخدم:

  • fetchPosts.pending
  • fetchPosts.fulfilled
  • fetchPosts.rejected

بدل ما نكتب string type يدويًا.

🧠 خلاصة عامة
  • Axios مسؤول عن الاتصال بالـ API فقط.
  • createAsyncThunk يغلّف منطق الـ async ويعطيك 3 حالات تلقائيًا.
  • extraReducers مكان التعامل مع هذه الحالات وتحديث state حسب النتيجة.
  • في الكمبوننت:
    • نستخدم useEffect لطلب البيانات.
    • نقرأ status, items, error.
    • نعرض UI مختلف لكل حالة (loading / success / error / idle).

لو حاب بعد كذا نطوّر المثال إلى:

  • إضافة post جديد باستخدام POST
  • أو حذف / تعديل
    أقدر أبني لك نفس الفكرة مع createAsyncThunk ثاني وشرح كامل.