1- Slice Code
import { createSlice , createAsyncThunk } from "@reduxjs/toolkit";
import api from "../api";
const initialState = {
data:[],
isLoading: false,
error: null,
meta: null,
}
export const getAllBooks = createAsyncThunk("books-actions" , async ({ page = 1, pageSize = 6 } = {})=> {
// const {data} = await api.get("/books?populate=*");
const {data} = await api.get(`/books?pagination[page]=${page}&pagination[pageSize]=${pageSize}&populate=*`);
return data;
});
const booksSlice = createSlice({
name: "booksSlice",
initialState,
extraReducers: (builder)=> {
builder.addCase(getAllBooks.pending , (state)=> {
state.isLoading = true;
state.error = null;
})
.addCase(getAllBooks.fulfilled , (state , action)=> {
state.data = action.payload.data;
state.isLoading = false;
state.meta = action.payload.meta;
})
.addCase(getAllBooks.rejected , (state , action)=> {
state.isLoading = false;
state.error = action.error.message || "حدث خطا في البيانات"
})
}
});
export const booksReducer = booksSlice.reducerJavaScript2- Book Page
const [currentPage, setCurrentPage] = useState(1);
const pageSize = 3;
useEffect(() => {
dipatch(getAllBooks({ page: currentPage, pageSize }))
}, [dipatch, data.length, currentPage]);
const totalPages = meta?.pagination?.pageCount || 1;JavaScript3- أزرار التنقل
{/* Pagination Buttons */}
<div className="d-flex justify-content-center mt-4">
{[...Array(totalPages)].map((_, index) => (
<button
key={index}
className={`btn mx-1 ${currentPage === index + 1
? "btn-primary"
: "btn-outline-primary"
}`}
onClick={() => setCurrentPage(index + 1)}
>
{index + 1}
</button>
))}
</div>JavaScriptسنرجع الآن ونشرح ربط رقم الصفحة بالـ URL بالتفصيل العميق خطوة خطوة حتى تفهمه معماريًا بالكامل.
🎯 الهدف النهائي
نريد أن يصبح الرابط هكذا:
/books?page=3JavaScriptوعند:
- الضغط على زر صفحة 4 → يتغير الرابط
- عمل Refresh → تبقى في نفس الصفحة
- الضغط Back → يرجع للصفحة السابقة
🔥 هذا هو السلوك الاحترافي.
🧠 أولاً: لماذا لا نستخدم useState فقط؟
لو كتبنا:
const [currentPage, setCurrentPage] = useState(1);JavaScriptالمشكلة:
- عند Refresh → يرجع إلى 1
- الرابط لا يتغير
- لا يمكن مشاركة الصفحة 3 مباشرة
لذلك نستخدم:
🔹 useSearchParams
بدل useState
🏗 الخطوة 1: قراءة الصفحة من الرابط
import { useSearchParams } from "react-router-dom";
const [searchParams, setSearchParams] = useSearchParams();
const currentPage = Number(searchParams.get("page")) || 1;JavaScript🧠 ماذا يحدث هنا بالتفصيل؟
1️⃣ useSearchParams()
هذا Hook من React Router.
يرجع Array:
[URLSearchParams, setSearchParams]JavaScript- الأول: كائن يمثل query params
- الثاني: دالة لتعديلهم
2️⃣ searchParams.get(“page”)
لو الرابط:
/books?page=4JavaScriptفإن:
searchParams.get("page")JavaScriptيرجع:
"4"JavaScript⚠️ يرجع String وليس Number.
3️⃣ Number()
نحوّلها إلى رقم:
Number("4") → 4JavaScript4️⃣ || 1
لو لم يوجد page في الرابط:
/booksJavaScriptفـ get(“page”) → null
Number(null) → 0
نريد أن تكون الصفحة الافتراضية = 1
لذلك نكتب:
Number(searchParams.get("page")) || 1;JavaScript🔥 الآن currentPage دائمًا رقم صحيح.
🏗 الخطوة 2: جلب البيانات عند تغير الصفحة
useEffect(() => {
dispatch(getAllBooks({ page: currentPage, pageSize: 6 }));
}, [dispatch, currentPage]);JavaScript🧠 ماذا يحدث هنا؟
- عند تحميل الصفحة
- أو عند تغير currentPage
- يتم استدعاء API
⚠️ currentPage يتغير عندما يتغير الرابط
وليس عندما نغير state يدويًا.
🏗 الخطوة 3: تغيير الصفحة بتغيير الرابط
بدل:
setCurrentPage(3);JavaScriptنكتب:
setSearchParams({ page: 3 });JavaScript🧠 ماذا يفعل setSearchParams؟
لو كنا في:
/books?page=2JavaScriptوعملنا:
setSearchParams({ page: 3 });JavaScriptالرابط يصبح:
/books?page=3JavaScriptثم:
- currentPage يُعاد حسابه
- useEffect يعمل
- يتم جلب الصفحة الجديدة
🔥 الرابط هو مصدر الحقيقة.
🏗 استخدامه داخل Pagination
<button
onClick={() => setSearchParams({ page: index + 1 })}>
{index + 1}
</button>JavaScript🧠 ماذا يحدث عند الضغط؟
مثلاً ضغط المستخدم على 5:
1️⃣ يتم تنفيذ setSearchParams({ page: 5 })
2️⃣ الرابط يتغير
3️⃣ React Router يعيد render
4️⃣ currentPage يصبح 5
5️⃣ useEffect يعمل
6️⃣ dispatch(getAllBooks({ page: 5 }))
🔥 دورة كاملة مترابطة.