後端
- 由於我們並不是使用 Django template 來渲染前端頁面,所以並不能單純的用 Django 的
Paginator
來分頁
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| @api_view(['GET']) def getProducts(request): query = request.query_params.get('keyword') if query is None: query = '' products = Product.objects.all() page = request.query_params.get('page') paginator = Paginator(products, 2) try: products = paginator.page(page) except PageNotAnInteger: 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({'products': serializer.data, 'page': page, 'pages': paginator.num_pages})
|
目前的 Response
直接測試應會出現 products.map is not a function
的錯誤,因為目前後端返回的 API 內容已變成:

1 2 3 4 5
| { "products": [...], "page": 1, "pages": 5 }
|
不再是單純的products
,所以才需要更新 productListReducer
1 2 3 4 5 6 7 8 9 10 11
| 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 }; ... } };
|
為了測試分頁功能,需要暫時更改 productListReducer
的內容讓期能保存後端返回的完整資料結構,如 products、page 和 pages
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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 }; case PRODUCT_LIST_FAIL: return { loading: false, error: action.payload }; default: return state; } };
|
目前這樣進度直接訪問網站的話應只會出現 2 個產品,因為後端設定每頁只顯示 2 個產品:

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

前端
搜尋欄元件
這邊只需稍更改 navigate
的路由參數,確保每次搜尋產品時,自動從第 1 頁開始
1 2 3 4 5 6 7 8 9
| const submitHandler = (e) => { e.preventDefault(); if (keyword.trim()) { navigate(`/?keyword=${keyword}&page=1`); } else { navigate(window.location.pathname); } };
|
頁數按鈕元件
在 components
資料夾創建新元件 Paginate.js
,此元件共會傳進四個參數:
pages
:總頁數
page
:當前頁碼
keyword
:搜尋關鍵字,預設空字串,不一定每次都會帶有搜尋關鍵字
isAdmin
:是否為管理員
現在 react-router-bootstrap 不允許將查詢參數(?keyword=...&page=...)
直接作為 pathname 了
會報錯的寫法:
1 2 3
| <LinkContainer to={`/?keyword=${keyword}&page=${x + 1}`,} >
|
所有要分 pathname
和 search
分別賦值
另外,這邊有一個大重點就是當使用 <LinkContainer>
來包裹 <Pagination.Item>
時,<Pagination.Item>
的 active
會衝突,導致每個頁碼都會標記為 active 狀態
,但是用 href
又會有刷新頁面的問題。
所以這邊使用的方法是製作一個 handleClick
函數,當點擊頁碼時,透過 useNavigate
來改變路由,就能達到不重整頁面的情況下改變路由
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| 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});
1 2 3 4
| ... </Table> <Paginate pages={pages} page={page} isAdmin={true} />
|
所以回來修改判斷式,當 isAdmin
為 true
時,導向 /admin/productlist
,否則就維持原本路由
1 2 3 4 5
| ... const handleClick = (x) => { navigate(`${isAdmin ? "/admin/productlist" : "/"}/?keyword=${keyword}&page=${x}`); };
|
修好之後再回去按頁碼,會發現儘管路由有正常改變,但換頁時頁面卻沒有重新渲染、沒有動靜

這是因為在目前 ProductListPage
中,沒有給 dispatch(listProducts()); 傳入 keyword
,修改程式多監聽 keyword
的變化
1 2 3 4 5 6 7 8 9 10 11 12
| 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]);
|
若您覺得這篇文章對您有幫助,歡迎分享出去讓更多人看到⊂◉‿◉つ~
留言版