
本文详解在 React + Redux 架构下实现分页的两种主流方案:基于 API 支持的“服务端分页”(推荐)与全量加载后的“客户端分页”,涵盖状态设计、Redux Toolkit 实现、选择器逻辑及组件集成,助你掌握可控、可扩展的分页实践。
本文详解在 React + Redux 架构下实现分页的两种主流方案:基于 API 支持的“服务端分页”(推荐)与全量加载后的“客户端分页”,涵盖状态设计、Redux Toolkit 实现、选择器逻辑及组件集成,助你掌握可控、可扩展的分页实践。
在现代 React 应用中,分页不应只是 UI 交互,而应是数据流与状态管理的协同设计。根据后端 API 能力的不同,我们采用两种正交策略——优先推荐服务端分页(按需拉取),仅在特殊场景(如静态数据、调试原型)下采用客户端分页(前端计算切片)。以下以 Redux Toolkit 为基准,提供生产就绪的实现范式。
✅ 方案一:服务端分页(推荐 · 高性能 & 低带宽)
当 API 支持 limit 和 offset(或 page/size)参数时,分页逻辑应下沉至请求层:
// api/itemsApi.ts
export const fetchItems = createAsyncThunk<
Item[],
{ page: number; pageSize: number },
{ state: RootState }
>('items/fetch', async ({ page, pageSize }) => {
const response = await axios.get('/api/items', {
params: { offset: (page - 1) * pageSize, limit: pageSize }
});
return response.data;
});Redux 状态仅需跟踪分页元信息,不存储全部数据:
const initialState = {
items: [] as Item[],
currentPage: 1,
pageSize: 10,
total: 0, // 由 API 返回(如响应头或 data.meta.total)
loading: false,
error: null as string | null,
};配合 createSlice 管理翻页动作:
const itemsSlice = createSlice({
name: 'items',
initialState,
reducers: {
setPage: (state, action: PayloadAction<number>) => {
state.currentPage = Math.max(1, action.payload);
},
setPageSize: (state, action: PayloadAction<number>) => {
state.pageSize = action.payload;
state.currentPage = 1; // 重置页码
},
},
extraReducers: (builder) => {
builder
.addCase(fetchItems.pending, (state) => {
state.loading = true;
})
.addCase(fetchItems.fulfilled, (state, action) => {
state.items = action.payload;
state.loading = false;
});
},
});⚠️ 注意:服务端分页需后端返回总条数(如
Content-Range: items 0-9/123或{ data: [...], meta: { total: 123 } }),前端据此渲染页码控件。
✅ 方案二:客户端分页(全量加载后切片)
仅适用于数据量小(currentPage 计算视图切片:
// slices/paginationSlice.ts
const paginationSlice = createSlice({
name: 'pagination',
initialState: {
allItems: [] as Item[],
currentPage: 1,
itemsPerPage: 10,
} as const,
reducers: {
setAllItems: (state, action: PayloadAction<Item[]>) => {
state.allItems = action.payload;
state.currentPage = 1; // 加载新数据时重置页码
},
nextPage: (state) => {
const maxPage = Math.ceil(state.allItems.length / state.itemsPerPage);
if (state.currentPage < maxPage) state.currentPage += 1;
},
prevPage: (state) => {
if (state.currentPage > 1) state.currentPage -= 1;
},
setCurrentPage: (state, action: PayloadAction<number>) => {
const maxPage = Math.ceil(state.allItems.length / state.itemsPerPage);
state.currentPage = Math.max(1, Math.min(action.payload, maxPage));
},
},
});
// 选择器:安全计算当前页数据(自动处理越界)
export const selectPaginatedItems = createSelector(
(state: RootState) => state.pagination,
(pagination) => {
const { allItems, currentPage, itemsPerPage } = pagination;
const start = (currentPage - 1) * itemsPerPage;
return allItems.slice(start, start + itemsPerPage);
}
);在组件中使用:
function ItemList() {
const items = useSelector(selectPaginatedItems);
const { currentPage, allItems } = useSelector((s: RootState) => s.pagination);
const dispatch = useDispatch();
const totalPages = Math.ceil(allItems.length / 10);
return (
<div>
<ul>
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
<div className="pagination">
<button
onClick={() => dispatch(prevPage())}
disabled={currentPage === 1}
>
上一页
</button>
<span>第 {currentPage} 页 / 共 {totalPages} 页</span>
<button
onClick={() => dispatch(nextPage())}
disabled={currentPage === totalPages}
>
下一页
</button>
</div>
</div>
);
}? 关键实践建议
-
永远校验边界:
setCurrentPage必须限制在[1, totalPages]区间,避免空数组或崩溃; -
响应式 pageSize:提供每页条数切换(如 10/20/50),并同步重置
currentPage; -
加载状态反馈:服务端分页时,
fetchItems.pending应显示骨架屏或禁用按钮; -
URL 同步(进阶):结合
react-router@6的useSearchParams,将page和size同步到 URL,支持分享与浏览器前进/后退; -
避免冗余状态:客户端分页时,
totalPages应动态计算而非存入 state,减少维护成本。
分页的本质是数据获取策略与 UI 展示逻辑的解耦。选择服务端分页,是对可伸缩性的尊重;理解客户端分页,则是对 Redux 状态流的深度掌握。二者并非互斥,而是根据业务场景精准选用的工程权衡。


















