NextJS分页组件无限重复渲染及useEffect调用异常排查
NextJS react-bootstrap分页组件无限重渲染问题排查
初始问题代码
分页组件初始实现
import Pagination from 'react-bootstrap/Pagination'; import Router from "next/router"; import {useEffect} from "react"; export { Paging } function Paging(props) { let pages = [] const generatePages = () => { for (let page = 1; page <= props.paginator.totalPages; page++) { let url = `/${props.source}?page=${page}&per_page=${props.paginator.perPage}` console.log(url) pages.push( <Pagination.Item key={page} active={page == props.paginator.page } onClick={() => Router.push(url)}> { page } </Pagination.Item> ); } } generatePages(); return ( <div id="pagination"> <div className={"pagination-component"}> <Pagination> { props.paginator.prevPage != null && <Pagination.Prev onClick={() => Router.push(props.paginator.prevPage)} /> } { pages } { props.paginator.nextPage != null && <Pagination.Next onClick={() => Router.push(props.paginator.nextPage)}/> } </Pagination> </div> <div className={"total-pages"}> Showing {props.paginator.perPage} out of { props.paginator.totalCount } </div> </div> ) }
父组件(Genres列表页)代码
import Table from 'react-bootstrap/Table'; import { HiOutlineTrash, HiOutlinePencil } from "react-icons/hi"; import { getAllGenres } from "../../api/genre"; import Link from 'next/link' import Button from "react-bootstrap/Button"; import useStore from "../../stores"; import Alert from "react-bootstrap/Alert"; import { Paging } from "../../components/Pagination"; export default function Genres({ genres, pagination }) { const alerts = useStore((state) => state.alerts) const removeAllAlerts = useStore((state) => state.removeAllAlerts); const setShow = (show=false) => { removeAllAlerts(); } // automatically close any open alerts after 10 seconds (() => setTimeout(setShow, 10000))(); return ( <> { alerts.length > 0 && <Alert variant="success" onClose={() => setShow(false)} dismissible> <p> { alerts[0] } </p> </Alert> } <div className={"page-title-section"}> <h2 className={"page-title"}>Genres</h2> <div className={"add-new-top"}> <Link href={"/genres/new"}> <Button variant="dark">Add New Genre</Button> </Link> </div> </div> <Table striped bordered hover> <thead> <tr> <th>Name</th> <th>Description</th> <th>Parent</th> <th>Active</th> <th>Created At</th> <th/> </tr> </thead> <tbody> { genres.data && genres.data.map(genre => ( <tr key={genre.id}> <td> <Link href={`/genres/${genre.slug}`}> { genre.name } </Link> </td> <td>{ genre.description }</td> <td>{ genre.parent }</td> <td>{ genre.active ? "Yes" : "No" }</td> <td>{ genre['created_at'] }</td> <td> <Link href={"/"}> <a className={"action-btn edit-btn"}><HiOutlinePencil /></a> </Link> <Link href={"/"}> <a className={"action-btn remove-btn"}><HiOutlineTrash /></a> </Link> </td> </tr> ))} </tbody> </Table> <Paging paginator={pagination} source={"genres"} /> </> ) } export const getServerSideProps = async (context) => { const page = context.query.page; const perPage = context.query.perPage; let genres = {} const res = await getAllGenres(page, perPage); if (res.data !== undefined) { genres = res.data } let pagination = { totalPages: res.data["total_pages"], totalCount: res.data["total_count"], perPage: res.data["per_page"], page: res.data["page"], nextPage: res.data["next_page_link"], prevPage: res.data["prev_page_link"], } return { props: { genres, pagination, } }; };
现象记录
- 初始版本分页功能正常,但
generatePages内的日志在浏览器控制台无限重复打印 - 将
generatePages移入空依赖useEffect后,日志重复问题消失,但页码项完全不渲染

- 简化分页组件仅保留上下页按钮,在JSX中加入调试日志,依然无限打印
- Chrome控制台日志无限循环输出,NextJS终端相同日志仅打印2次


问题解答
日志有效性判断
浏览器端的无限打印是客户端真实的渲染状态。终端仅打印2次是NextJS服务端渲染+客户端首次水合的正常输出,后续的无限重渲染完全发生在客户端,服务端不会重复执行页面组件逻辑,因此没有额外日志输出。
根因分析
问题分为两部分:
- 页码不显示原因
pages是组件函数内的普通局部变量,每次组件渲染都会被重置为空数组。将generatePages放到useEffect中执行时,首次渲染阶段pages为空,JSX直接渲染空数组;等渲染完成后useEffect执行,给pagespush元素的操作不会触发React重渲染,组件不会重新执行return逻辑,因此页码永远无法显示。 - 无限重渲染根因
问题出在父组件中直接写在函数体里的立即执行定时器:
组件每次渲染都会同步执行这段逻辑注册新的定时器,定时器触发时会调用(() => setTimeout(setShow, 10000))();removeAllAlerts更新全局状态,状态变更会触发所有订阅了zustand store的组件重渲染,Genres组件因为订阅了alerts状态会重新渲染,重渲染又会注册新的定时器,形成死循环。如果实际代码中误将setShow加了括号写成setTimeout(setShow(), 10000),会在渲染阶段立刻执行状态更新,页面一加载就会进入无限循环,和观察到的日志立刻无限打印的现象完全吻合。
修复方案
- 修复父组件无限循环
将定时器逻辑移入useEffect,添加依赖和清理函数,避免渲染阶段重复注册定时器:// 替换原来的立即执行定时器代码 useEffect(() => { if (alerts.length > 0) { const timer = setTimeout(() => { removeAllAlerts(); }, 10000); // 组件卸载或依赖变化时清理已有定时器 return () => clearTimeout(timer); } }, [alerts, removeAllAlerts]); - 修复分页组件页码不显示
不要用普通局部变量+useEffect的方式生成渲染内容,直接在渲染阶段计算页码,可用useMemo做性能优化:import Pagination from 'react-bootstrap/Pagination'; import Router from "next/router"; import { useMemo } from "react"; export function Paging(props) { const pages = useMemo(() => { const pageItems = []; for (let page = 1; page <= props.paginator.totalPages; page++) { const url = `/${props.source}?page=${page}&per_page=${props.paginator.perPage}`; pageItems.push( <Pagination.Item key={page} active={page === props.paginator.page} onClick={() => Router.push(url)} > {page} </Pagination.Item> ); } return pageItems; }, [props.paginator, props.source]); return ( <div id="pagination"> <div className="pagination-component"> <Pagination> {props.paginator.prevPage != null && <Pagination.Prev onClick={() => Router.push(props.paginator.prevPage)} /> } {pages} {props.paginator.nextPage != null && <Pagination.Next onClick={() => Router.push(props.paginator.nextPage)} /> } </Pagination> </div> <div className="total-pages"> Showing {props.paginator.perPage} out of {props.paginator.totalCount} </div> </div> ) }
内容的提问来源于stack exchange,提问作者MrCujo
相关产品推荐
相关产品推荐

