# 後端
- 由於我們並不是使用 Django template 來渲染前端頁面,所以並不能單純的用 Django 的
Paginator來分頁
@api_view(['GET'])
def getProducts(request):
query = request.query_params.get('keyword')
# print("query:", query)
if query is None:
query = ''
products = Product.objects.all()
page = request.query_params.get('page')
paginator = Paginator(products, 2) # 每頁顯示 2 個產品
try:
products = paginator.page(page)
except PageNotAnInteger: # 如果 page 不是整數,則預設為 1
products = paginator.page(1)
except EmptyPage:
products = paginator.page(paginator.num_pages)
if page is None:
page = 1
page = int(page)
print("page:", page)
else:
products = Product.objects.filter(name__icontains=query)
serializer = ProductSerializer(products, many=True)
# return Response(serializer.data)
return Response({'products': serializer.data, 'page': page, 'pages': paginator.num_pages})
目前的 Response 直接測試應會出現 products.map is not a function 的錯誤,因為目前後端返回的 API 內容已變成:

{
"products": [...], // 產品列表
"page": 1, // 當前頁碼
"pages": 5 // 總頁數
}
不再是單純的products,所以才需要更新 productListReducer
export const productListReducer = (state = { products: [] }, action) => {
// 列出所有產品
switch (action.type) {
case PRODUCT_LIST_REQUEST: // 請求中
return { loading: true, products: [] };
case PRODUCT_LIST_SUCCESS: // 成功
return { loading: false, products: action.payload }; // 請求到資料了=loading結束
...
}
};
為了測試分頁功能,需要暫時更改 productListReducer 的內容讓期能保存後端返回的完整資料結構,如 products、page 和 pages
export const productListReducer = (state = { products: [] }, action) => {
// 列出所有產品
switch (action.type) {
case PRODUCT_LIST_REQUEST: // 請求中
return { loading: true, products: [] };
case PRODUCT_LIST_SUCCESS: // 成功
return { loading: false, products: action.payload.products, page: action.payload.page, pages: action.payload.pages }; // 請求到資料,loading 結束
case PRODUCT_LIST_FAIL: // 錯誤
return { loading: false, error: action.payload };
default:
return state;
}
};
目前這樣進度直接訪問網站的話應只會出現 2 個產品,因為後端設定每頁只顯示 2 個產品:

而目前只能手動在網址中輸入 ?page=2 來查看其他頁面的產品,所以接下來要在前端實現分頁功能

# 前端
# 搜尋欄元件
這邊只需稍更改 navigate 的路由參數,確保每次搜尋產品時,自動從第 1 頁開始
// SearchBar.js
const submitHandler = (e) => {
e.preventDefault();
if (keyword.trim()) {
navigate(`/?keyword=${keyword}&page=1`); // 這邊 ?keyword 不用加任何其他東西,否則 HomePage 的 useLocation 印不出 keyword
} else {
navigate(window.location.pathname); // 若沒有輸入關鍵字,則停留在當前頁面
}
};
# 頁數按鈕元件
在 components 資料夾創建新元件 Paginate.js,此元件共會傳進四個參數:
pages:總頁數page:當前頁碼keyword:搜尋關鍵字,預設空字串,不一定每次都會帶有搜尋關鍵字isAdmin:是否為管理員
現在 react-router-bootstrap 不允許將查詢參數(?keyword=...&page=...)直接作為 pathname 了
會報錯的寫法:
<LinkContainer
to={`/?keyword=${keyword}&page=${x + 1}`,}
>
所有要分 pathname 和 search 分別賦值
另外,這邊有一個大重點就是當使用 <LinkContainer> 來包裹 <Pagination.Item> 時,<Pagination.Item> 的 active 會衝突,導致每個頁碼都會標記為 active 狀態 ,但是用 href 又會有刷新頁面的問題。
所以這邊使用的方法是製作一個 handleClick 函數,當點擊頁碼時,透過 useNavigate 來改變路由,就能達到不重整頁面的情況下改變路由
// Paginate.js
import React from "react";
import { Pagination } from "react-bootstrap";
import { useNavigate } from "react-router-dom";
const Paginate = ({ pages, page, keyword = "" }) => {
const navigate = useNavigate();
if (keyword) {
keyword = keyword.split("?keyword=")[1]?.split("&")[0] || "";
}
page = Number(page) || 1;
const handleClick = (x) => {
navigate(`/?keyword=${keyword}&page=${x}`);
};
return (
<Pagination>
{[...Array(pages).keys()].map((x) => (
<Pagination.Item key={x + 1} active={x + 1 === page} onClick={() => handleClick(x + 1)}>
{x + 1}
</Pagination.Item>
))}
</Pagination>
);
};
export default Paginate;
- 用展開運算子(Spread operator)
[...Array()將Iterator轉換為普通的陣列,因為.keys() 會返回 Iterator,這樣才能進行 map() 運算。
📌 總結
方法 結果 備註
Array(pages).keys() Array Iterator {} Iterator,不是陣列
[...Array(pages).keys()] [0, 1, 2, 3, 4] 用 ... 展開成陣列
Array.from(Array(pages).keys()) [0, 1, 2, 3, 4] 直接轉換成陣列
👉 結論:因為 .keys() 回傳的是 Iterator,不能直接當作陣列使用,因此需要 ... 或 Array.from() 來展開它!
# 產品管理列表(Admin 權限專用)
將 Paginate 元件直接加入到後,並將 isAdmin 設為 true,此時若直接點擊頁碼會發現跳回首頁了,這是因為 Paginate 元件的 handleClick 函數中目前是只導向 navigate(/?keyword={keyword}&page={x});
// ProductListPage.js
...
</Table>
<Paginate pages={pages} page={page} isAdmin={true} />
所以回來修改判斷式,當 isAdmin 為 true 時,導向 /admin/productlist,否則就維持原本路由
// Paginate.js
...
const handleClick = (x) => {
navigate(`${isAdmin ? "/admin/productlist" : "/"}/?keyword=${keyword}&page=${x}`);
};
修好之後再回去按頁碼,會發現儘管路由有正常改變,但換頁時頁面卻沒有重新渲染、沒有動靜

這是因為在目前 ProductListPage 中,沒有給 dispatch(listProducts()); 傳入 keyword,修改程式多監聽 keyword 的變化
// ProductListPage.js
let keyword = useLocation().search;
...
useEffect(() => {
dispatch({ type: PRODUCT_CREATE_RESET }); // 進入頁面先重置新增產品狀態
if (successCreate) {
// 若成功新增產品,導向到該產品編輯頁面
navigate(`/admin/product/${createdProduct.id}/edit`);
} else {
dispatch(listProducts(keyword));
}
}, [dispatch, navigate, successDelete, successCreate, createdProduct, keyword]);
