
本文详解如何在 react + redux 应用中实现健壮、可维护的分页逻辑,涵盖服务端分页(推荐)和前端内存分页两种主流模式,并提供完整 slice 结构、selector 计算逻辑及组件集成示例。
本文详解如何在 react + redux 应用中实现健壮、可维护的分页逻辑,涵盖服务端分页(推荐)和前端内存分页两种主流模式,并提供完整 slice 结构、selector 计算逻辑及组件集成示例。
在构建数据密集型 React 应用时,分页不仅是用户体验的关键,更是性能优化的必选项。Redux 本身不内置分页能力,但通过合理设计状态结构与数据流,可轻松支撑两种典型场景:服务端分页(API 支持 limit/offset 或 page/size)与前端内存分页(API 返回全量数据,由前端切片展示)。下面以现代 Redux Toolkit(RTK)为基准,给出生产就绪的实现方案。
✅ 方案一:服务端分页(推荐,适用于大多数真实项目)
当后端 API 支持分页参数(如 /api/items?page=2&size=10),前端只需管理当前页码、每页条数,并触发带参数的请求:
// features/pagination/paginationSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
interface PaginationState {
currentPage: number;
itemsPerPage: number;
totalItems?: number; // 可选:用于计算总页数
loading: boolean;
error: string | null;
}
const initialState: PaginationState = {
currentPage: 1,
itemsPerPage: 10,
loading: false,
error: null,
};
export const paginationSlice = createSlice({
name: 'pagination',
initialState,
reducers: {
setPage: (state, action: PayloadAction<number>) => {
state.currentPage = Math.max(1, action.payload);
},
setItemsPerPage: (state, action: PayloadAction<number>) => {
state.itemsPerPage = Math.max(1, action.payload);
state.currentPage = 1; // 切换每页数量时重置到第 1 页
},
setLoading: (state, action: PayloadAction<boolean>) => {
state.loading = action.payload;
},
setError: (state, action: PayloadAction<string null>) => {
state.error = action.payload;
},
setTotalItems: (state, action: PayloadAction<number>) => {
state.totalItems = action.payload;
},
},
});
export const {
setPage,
setItemsPerPage,
setLoading,
setError,
setTotalItems,
} = paginationSlice.actions;
export default paginationSlice.reducer;</number></string></boolean></number></number>
配合 Thunk 或 RTK Query,在获取数据时动态拼接参数:
// 假设使用 RTK Query
// api/endpoints/itemsApi.ts
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react';
export const itemsApi = createApi({
baseQuery: fetchBaseQuery({ baseUrl: '/api' }),
endpoints: (builder) => ({
getItems: builder.query<item page: number size:>({
query: ({ page, size }) => `/items?page=${page}&size=${size}`,
// 可选:自动更新 totalItems
async onQueryStarted(_, { dispatch, queryFulfilled }) {
try {
const { data } = await queryFulfilled;
// 若响应含 meta.total,可同步更新 pagination 状态
dispatch(setTotalItems(data.meta?.total || 0));
} catch {}
},
}),
}),
});</item>
组件中使用:
import { useSelector, useDispatch } from 'react-redux';
import { useGetItemsQuery } from '@/api/endpoints/itemsApi';
import { setPage, setItemsPerPage } from '@/features/pagination/paginationSlice';
function ItemList() {
const dispatch = useDispatch();
const { currentPage, itemsPerPage } = useSelector((state: RootState) => state.pagination);
const { data, isLoading, isError } = useGetItemsQuery({ page: currentPage, size: itemsPerPage });
const totalPages = useSelector((state: RootState) =>
state.pagination.totalItems ? Math.ceil(state.pagination.totalItems / itemsPerPage) : 1
);
const handlePageChange = (page: number) => {
if (page >= 1 && page
{isLoading && <p>Loading...</p><div class="aritcle_card flexRow artxards">
<div class="artcardd flexRow">
<a class="aritcle_card_img" rel="nofollow" href="/xiazai/skill5092" title="PigX UI 前端开发"><img
src="https://img.php.cn/upload/skill/000/000/081/179033410052138.jpg" alt="PigX UI 前端开发" onerror="this.onerror='';this.src='/static/lhimages/moren/morentu.png'" ></a>
<div class="aritcle_card_info flexColumn">
<a rel="nofollow" href="/xiazai/skill5092" title="PigX UI 前端开发" class="overflowclass">PigX UI 前端开发</a>
<p class="overflowclass">PigX UI Pro 前端开发指南 - Vue 3 + TypeScript + Element Plus。当用户提到 PigX UI、PigX 前端、lgb-mgui 项目、Vue 3 企业级后台开发、Element Plus 后台开发时使用此技能。</p>
</div>
<a rel="nofollow" href="/xiazai/skill5092" title="PigX UI 前端开发" class="aritcle_card_btn flexRow flexcenter"><b></b><span>下载</span>
</a>
</div>
</div>}
{isError && <p>Error loading items.</p>}
{data?.map(item => (
<div key="{item.id}">{item.title}</div>
))}
{/* 分页控件 */}
<div classname="pagination">
<button onclick="{()"> handlePageChange(currentPage - 1)} disabled={currentPage
Previous
</button>
<span>Page {currentPage} of {totalPages}</span>
<button onclick="{()"> handlePageChange(currentPage + 1)} disabled={currentPage >= totalPages}>
Next
</button>
</div>
);
}
export default ItemList;
✅ 方案二:前端内存分页(仅限小数据集或原型验证)
当后端无法分页(返回全部 5000 条数据),且你确认内存开销可控时,可采用纯前端切片方式。此时需将 items 存入 Redux(或作为缓存状态),并通过 Selector 实时计算当前页数据:
// features/pagination/memoryPaginationSlice.ts
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
interface MemoryPaginationState {
items: any[]; // 替换为具体类型
currentPage: number;
itemsPerPage: number;
}
const initialState: MemoryPaginationState = {
items: [],
currentPage: 1,
itemsPerPage: 10,
};
export const memoryPaginationSlice = createSlice({
name: 'memoryPagination',
initialState,
reducers: {
setItems: (state, action: PayloadAction<any>) => {
state.items = action.payload;
state.currentPage = 1; // 加载新数据时重置页码
},
nextPage: (state) => {
const maxPage = Math.ceil(state.items.length / state.itemsPerPage);
if (state.currentPage {
if (state.currentPage > 1) state.currentPage -= 1;
},
goToPage: (state, action: PayloadAction<number>) => {
const maxPage = Math.ceil(state.items.length / state.itemsPerPage);
state.currentPage = Math.max(1, Math.min(action.payload, maxPage));
},
setItemsPerPage: (state, action: PayloadAction<number>) => {
state.itemsPerPage = Math.max(1, action.payload);
state.currentPage = 1;
},
},
});
// Selector:安全切片,避免越界
export const selectCurrentPageItems = (state: RootState) => {
const { items, currentPage, itemsPerPage } = state.memoryPagination;
const startIndex = (currentPage - 1) * itemsPerPage;
return items.slice(startIndex, startIndex + itemsPerPage);
};</number></number></any>
组件中消费:
import { useSelector } from 'react-redux';
import { selectCurrentPageItems } from '@/features/pagination/memoryPaginationSlice';
function MemoryPaginatedList() {
const items = useSelector(selectCurrentPageItems);
const { currentPage, itemsPerPage } = useSelector((state: RootState) => state.memoryPagination);
const totalPages = Math.ceil(useSelector((s: RootState) => s.memoryPagination.items.length) / itemsPerPage);
return (
<div>
{items.map(item => (
<div key="{item.id}">{item.name}</div>
))}
<div classname="pagination">
<button onclick="{()"> dispatch(prevPage())} disabled={currentPage ← Prev</button>
<span>{currentPage} / {totalPages}</span>
<button onclick="{()"> dispatch(nextPage())} disabled={currentPage >= totalPages}>Next →</button>
</div>
</div>
);
}
⚠️ 注意事项与最佳实践
- 永远优先选择服务端分页:避免前端加载冗余数据,降低首屏时间与内存压力;
-
防抖页码变更:在用户快速点击“下一页”时,可结合
throttle或取消未完成请求; -
保持状态一致性:切换
itemsPerPage时应重置currentPage = 1,否则可能出现空页; -
总页数计算容错:
Math.ceil(total / perPage)在total = 0时需兜底为1; -
TypeScript 类型强化:为
items使用泛型接口(如Item[]),提升可维护性; - 不要在 reducer 中执行副作用(如 API 调用),交由 Thunk 或 RTK Query 处理。
通过以上两种模式的清晰划分与代码封装,你不仅能灵活应对不同后端能力,还能深入理解 Redux 状态驱动 UI 的核心思想——分页的本质,是「状态映射到视图」的精准表达。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!










