createAsyncThunk 2

import toast, { Toaster } from 'react-hot-toast';
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import axios from 'axios';
const initialState = {
    data: [],
    isLoading: false,
    error:null
}

export const getAllPosts = createAsyncThunk("posts-all" ,async ()=>{
    const {data} = await axios.get("https://dummyjson.com/products");
    return data.products;
   });

const postsSlice = createSlice({
   name:"posts",
    initialState,
    reducers: {

    },
    extraReducers: (builder)=> {
        builder.addCase(getAllPosts.fulfilled , (state , action)=> {
            toast.error('تم جلب البيانات بنجاح ')
            state.data = action.payload;
            state.isLoading = false;

        });

        builder.addCase(getAllPosts.pending , (state)=> {
                  state.isLoading = true;
        });
    }
});

export const postsReducer = postsSlice.reducer
JavaScript
import { useSelector , useDispatch} from "react-redux"
import { useState ,useEffect } from "react";
import { getAllPosts } from "../features/posts/postsSlice";
import { data } from "react-router";

function Card() {
    const {data , isLoading} = useSelector((state)=> state.posts)
   
    const dispatch = useDispatch();
    useEffect(()=> {
        dispatch(getAllPosts());
    },[]);

   if(isLoading) {
    return <h1>Loading</h1>
   }
   
  return (
    <div>

    {data.length ? (data.map((post)=> (
        <h1>{post.title}</h1>
    ))) : ("No")}

    </div>
  )
}

export default Card
JavaScript
0️⃣ أوّل شيء: مشكلة صغيرة في الـ import

أنت كاتب:

import toast, { Toaster } from 'react-hot-toast';
JavaScript

في react-hot-toast الصحيح هو:

import { Toaster, toast } from 'react-hot-toast';
JavaScript
  • المكتبة ما عندها default export اسمه toast.
  • عندها named exports: واحد اسمه Toaster وواحد اسمه toast.
  • عشان كذا لازم تحط الاثنين بين { }.

إذن غيّر السطر الأوّل إلى:

import { Toaster, toast } from 'react-hot-toast';
JavaScript

وباقي الكود يمشي عادي مع toast.error(...).

(وبرضه الجملة اللي في التوست تقول “تم جلب البيانات بنجاح” والأفضل تخليها toast.success 😉، راح أرجع لها بعدين.)

1️⃣ الـ imports

import { Toaster, toast } from 'react-hot-toast';
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
import axios from 'axios';
JavaScript
ماذا تعني؟
  • Toaster
    كومبوننت React تعرض التوست في الشاشة (تحطه في App.jsx عادة).
  • toast
    دالة نستدعيها لما نحب نعرض رسالة:
    • toast.success('...')
    • toast.error('...')
    • toast('رسالة عادية')
  • createAsyncThunk
    من Redux Toolkit، تُستخدم لإنشاء action async (طلب API مثلًا) وتدير لك:
    • pending (جاري)
    • fulfilled (تم بنجاح)
    • rejected (صار خطأ)
  • createSlice
    دالة تنشئ لك slice من الـ state بكل ما يحتاج:
    • الاسم (name)
    • الحالة الابتدائية (initialState)
    • الـ reducers العادية
    • الـ extraReducers للـ async
  • axios
    مكتبة طلبات HTTP.
2️⃣ الحالة الابتدائية (initialState)
const initialState = {
    data: [],
    isLoading: false,
    error: null
}
JavaScript
شرح كل أتربيوت:
  • data
    • نوعه: مصفوفة [].
    • الهدف: تخزن فيها البيانات اللي جايه من الـ API (هنا products).
  • isLoading
    • نوعه: Boolean (true / false).
    • الهدف: نعرف هل حاليًا في طلب شغال ولا لأ.
      • إذا true → نعرض “جاري التحميل…” أو سبينر.
      • إذا false → نخفي التحميل.
  • error
    • نوعه: نص أو null.
    • الهدف: نخزن فيه رسالة الخطأ لو الطلب فشل.
    • إذا ما فيه خطأ → نخليه null.

هذه الثلاثة هي الشكل التقليدي لأي state لطلب API:
data + isLoading + error.

3️⃣ تعريف الـ thunk: جلب كل البوستات
export const getAllPosts = createAsyncThunk(
  "posts-all",
  async () => {
    const { data } = await axios.get("https://dummyjson.com/products");
    return data.products;
  }
);
JavaScript
ماذا يعني هذا؟
  • createAsyncThunk تستقبل باراميترين:
    1. نوع الـ action (type prefix)"posts-all" هذا النص يستخدمه Redux Toolkit لبناء أنواع الـ actions:
      • posts-all/pending
      • posts-all/fulfilled
      • posts-all/rejected
    2. دالة async async () => { ... } هذه هي الدالة اللي بداخلها تكتب منطق الطلب (Axios).

داخل الدالة:

const {data} = await axios.get("https://dummyjson.com/products");
return data.products;
JavaScript
  • axios.get(...) ترجع object مثل: { data: { products: [...] , ... }, status: 200, ... }
  • نستخدم destructuring: const { data } = await axios.get(...); الآن data = محتوى الـ data من الـ API.
  • ثم: return data.products; هذا اللي نرجّعه من الـ thunk، وفي حالة النجاح:
    • القيمة هذه تروح إلى: action.payload داخل fulfilled.

يعني لو استدعينا:

dispatch(getAllPosts());
JavaScript

وصار الطلب ناجح → داخل الـ reducer:

action.payload === data.products
JavaScript
4️⃣ إنشاء الـ slice
const postsSlice = createSlice({
   name:"posts",
   initialState,
   reducers: {},
   extraReducers: (builder)=> {
     ...
   }
});
JavaScript
شرح كل أتربيوت:
  • name: "posts"
    • اسم هذا الـ slice.
    • يُستخدم كجزء من اسم الـ action في الحالة العادية.
    • هنا لأن الـ actions async جاية من createAsyncThunk باسم posts-all، فالاسم أهم شيء فقط لتنظيم state و DevTools.
  • initialState
    • نمرّر الكائن اللي عرّفناه قبل شوي.
    • هذا يصبح الشكل الابتدائي لـ state.posts (لو سجلته في الـ store باسم posts).
  • reducers: {}
    • مكان نعرّف فيه الـ reducers العادية (غير async) مثل addPost, removePost.
    • الآن عندك {} فاضي، يعني ما عندك reducers sync حاليًا.
  • extraReducers: (builder) => {...}
    • هنا نتعامل مع الـ actions القادمة من خارج الـ slice، مثل:
      • Thunks اللي أنشأناها بـ createAsyncThunk (getAllPosts).
    • نستخدم builder.addCase لكل حالة نبي نعالجها.
5️⃣ التعامل مع حالات الـ thunk في extraReducers
5.1 حالة النجاح (fulfilled)
builder.addCase(getAllPosts.fulfilled , (state , action)=> {
    toast.error('تم جلب البيانات بنجاح ')
    state.data = action.payload;
    state.isLoading = false;
});
JavaScript
  • getAllPosts.fulfilled
    هذا يعبّر عن الـ action النوع posts-all/fulfilled.
  • (state, action) => { ... }
    دالة الـ reducer اللي تنفّذ لما يوصل هذا الـ action.
    • state
      • هذا هو جزء الـ state الخاص بهذا الـ slice، شكله تقريبًا: { data: [], isLoading: false, error: null }
      • تقدر تعدّل عليه مباشرة في Redux Toolkit (بفضل Immer).
    • action
      • الكائن اللي يحتوي: { type: 'posts-all/fulfilled', payload: data.products, ... }
  • داخل الدالة: toast.error('تم جلب البيانات بنجاح ')
    • هنا تستدعي توست من المكتبة.
    • المفروض تكون toast.success بدل toast.error بما إن الرسالة تقول “تم جلب البيانات بنجاح” 😄
    state.data = action.payload;
    • نحط البيانات في state.data.
    • بما أن action.payload = data.products، الآن: state.data = [ ... قائمة المنتجات ... ]
    state.isLoading = false;
    • بما إن الطلب خلص بنجاح، نوقف حالة التحميل.

ملاحظة: الأفضل أيضًا هنا نعيد state.error = null; عشان نمسح أي خطأ قديم لو كان موجود.

5.2 حالة الانتظار (pending)
builder.addCase(getAllPosts.pending , (state)=> {
    state.isLoading = true;
});
JavaScript
  • getAllPosts.pending
    يعبر عن الـ action posts-all/pending الذي يُرسل تلقائيًا عند بداية التنفيذ: dispatch(getAllPosts()); // → يرسل pending مباشرة
  • (state) => { ... }
    • ما نحتاج action هنا، فقط نعدّل حالة التحميل:
    state.isLoading = true;
    • هذا يعني: “فيه طلب الآن شغال”
    • في الواجهة تقدر تستخدمه مثلًا: const isLoading = useSelector((state) => state.posts.isLoading); {isLoading && <p>جاري تحميل البيانات...</p>}
⚠️ 5.3 حالة الخطأ (rejected) – غير موجودة حاليًا

أنت ما كتبتها، لكن من الأفضل تضيفها:

builder.addCase(getAllPosts.rejected , (state, action)=> {
    state.isLoading = false;
    state.error = action.error.message || 'حدث خطأ في جلب البيانات';
    toast.error(state.error);
});
JavaScript
  • getAllPosts.rejected
    يوافق الـ action posts-all/rejected عندما يحصل خطأ في الطلب.
  • state.isLoading = false;
    نوقف التحميل.
  • state.error = ...;
    نخزن رسالة الخطأ لنعرضها في الواجهة.
  • toast.error(...)
    نعرض خطأ للمستخدم.

6️⃣ تصدير الـ reducer

export const postsReducer = postsSlice.reducer;
JavaScript
  • postsSlice.reducer
    هذا هو الـ function اللي يجمع كل الـ reducers و extraReducers اللي فوق في واحد.
  • نصدّره باسم postsReducer حتى نستخدمه في الـ store: import { configureStore } from '@reduxjs/toolkit'; import { postsReducer } from './postsSlice'; export const store = configureStore({ reducer: { posts: postsReducer, }, });
  • الآن في الـ state العام للتطبيق، عندك: state.posts = { data: [...], isLoading: false, error: null }
7️⃣ ربطه بالواجهة (ملخّص سريع جدًا)

في أي كمبوننت:

const dispatch = useDispatch();
const { data, isLoading, error } = useSelector((state) => state.posts);

useEffect(() => {
  dispatch(getAllPosts());
}, [dispatch]);
JavaScript

وفي الـ JSX:

{isLoading && <p>جاري تحميل البيانات...</p>}
{error && <p style={{color:'red'}}>{error}</p>}

<ul>
  {data.map((product) => (
    <li key={product.id}>{product.title}</li>
  ))}
</ul>
JavaScript
خلاصة نهائية 🧠
  • initialState
    • يحدد شكل البيانات اللي بنخزنها لهذا الـ slice:
      • data → النتائج
      • isLoading → حالة التحميل
      • error → رسالة الخطأ
  • getAllPosts
    • createAsyncThunk تعمل:
      • pending → أول ما يبدأ الطلب
      • fulfilled → إذا نجح (وتضع النتيجة في action.payload)
      • rejected → إذا فشل
  • createSlice
    • name → اسم منطقي للـ slice (يظهر في DevTools)
    • initialState → الحالة الابتدائية
    • reducers → أكشنات sync (ما استخدمناها هنا)
    • extraReducers → التعامل مع نتائج الـ thunks (pending / fulfilled / rejected)
  • داخل fulfilled:
    • نكتب: state.data = action.payload لتحديث البيانات.
    • و state.isLoading = false لإيقاف التحميل.
    • ونستدعي toast.success أو toast.error حسب الحالة.