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.reducerJavaScriptimport { 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 CardJavaScript0️⃣ أوّل شيء: مشكلة صغيرة في الـ 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→ نخفي التحميل.
- إذا
- نوعه: Boolean (
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تستقبل باراميترين:- نوع الـ action (type prefix)
"posts-all"هذا النص يستخدمه Redux Toolkit لبناء أنواع الـ actions:posts-all/pendingposts-all/fulfilledposts-all/rejected
- دالة async
async () => { ... }هذه هي الدالة اللي بداخلها تكتب منطق الطلب (Axios).
- نوع الـ action (type prefix)
داخل الدالة:
const {data} = await axios.get("https://dummyjson.com/products");
return data.products;
JavaScriptaxios.get(...)ترجع object مثل:{ data: { products: [...] , ... }, status: 200, ... }- نستخدم destructuring:
const { data } = await axios.get(...);الآنdata= محتوى الـ data من الـ API. - ثم:
return data.products;هذا اللي نرجّعه من الـ thunk، وفي حالة النجاح:- القيمة هذه تروح إلى:
action.payloadداخلfulfilled.
- القيمة هذه تروح إلى:
يعني لو استدعينا:
JavaScriptdispatch(getAllPosts());وصار الطلب ناجح → داخل الـ reducer:
JavaScriptaction.payload === data.products
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 حاليًا.
- مكان نعرّف فيه الـ reducers العادية (غير async) مثل
extraReducers: (builder) => {...}- هنا نتعامل مع الـ actions القادمة من خارج الـ slice، مثل:
- Thunks اللي أنشأناها بـ
createAsyncThunk(getAllPosts).
- Thunks اللي أنشأناها بـ
- نستخدم
builder.addCaseلكل حالة نبي نعالجها.
- هنا نتعامل مع الـ actions القادمة من خارج الـ slice، مثل:
5️⃣ التعامل مع حالات الـ thunk في extraReducers
5.1 حالة النجاح (fulfilled)
builder.addCase(getAllPosts.fulfilled , (state , action)=> {
toast.error('تم جلب البيانات بنجاح ')
state.data = action.payload;
state.isLoading = false;
});
JavaScriptgetAllPosts.fulfilled
هذا يعبّر عن الـ action النوعposts-all/fulfilled.(state, action) => { ... }
دالة الـ reducer اللي تنفّذ لما يوصل هذا الـ action.state- هذا هو جزء الـ state الخاص بهذا الـ slice، شكله تقريبًا:
{ data: [], isLoading: false, error: null } - تقدر تعدّل عليه مباشرة في Redux Toolkit (بفضل Immer).
- هذا هو جزء الـ state الخاص بهذا الـ slice، شكله تقريبًا:
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;
});JavaScriptgetAllPosts.pending
يعبر عن الـ actionposts-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);
});JavaScriptgetAllPosts.rejected
يوافق الـ actionposts-all/rejectedعندما يحصل خطأ في الطلب.state.isLoading = false;
نوقف التحميل.state.error = ...;
نخزن رسالة الخطأ لنعرضها في الواجهة.toast.error(...)
نعرض خطأ للمستخدم.
6️⃣ تصدير الـ reducer
export const postsReducer = postsSlice.reducer;JavaScriptpostsSlice.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→ رسالة الخطأ
- يحدد شكل البيانات اللي بنخزنها لهذا الـ slice:
getAllPostscreateAsyncThunkتعمل:pending→ أول ما يبدأ الطلبfulfilled→ إذا نجح (وتضع النتيجة فيaction.payload)rejected→ إذا فشل
createSlicename→ اسم منطقي للـ slice (يظهر في DevTools)initialState→ الحالة الابتدائيةreducers→ أكشنات sync (ما استخدمناها هنا)extraReducers→ التعامل مع نتائج الـ thunks (pending / fulfilled / rejected)
- داخل
fulfilled:- نكتب:
state.data = action.payloadلتحديث البيانات. - و
state.isLoading = falseلإيقاف التحميل. - ونستدعي
toast.successأوtoast.errorحسب الحالة.
- نكتب: