You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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后,日志重复问题消失,但页码项完全不渲染
    初始正常渲染的分页组件
    useEffect修改后页码不显示
  • 简化分页组件仅保留上下页按钮,在JSX中加入调试日志,依然无限打印
  • Chrome控制台日志无限循环输出,NextJS终端相同日志仅打印2次
    Chrome控制台无限打印日志
    NextJS终端仅打印2次日志

问题解答

日志有效性判断

浏览器端的无限打印是客户端真实的渲染状态。终端仅打印2次是NextJS服务端渲染+客户端首次水合的正常输出,后续的无限重渲染完全发生在客户端,服务端不会重复执行页面组件逻辑,因此没有额外日志输出。

根因分析

问题分为两部分:

  1. 页码不显示原因
    pages是组件函数内的普通局部变量,每次组件渲染都会被重置为空数组。将generatePages放到useEffect中执行时,首次渲染阶段pages为空,JSX直接渲染空数组;等渲染完成后useEffect执行,给pagespush元素的操作不会触发React重渲染,组件不会重新执行return逻辑,因此页码永远无法显示。
  2. 无限重渲染根因
    问题出在父组件中直接写在函数体里的立即执行定时器:
    (() => setTimeout(setShow, 10000))();
    
    组件每次渲染都会同步执行这段逻辑注册新的定时器,定时器触发时会调用removeAllAlerts更新全局状态,状态变更会触发所有订阅了zustand store的组件重渲染,Genres组件因为订阅了alerts状态会重新渲染,重渲染又会注册新的定时器,形成死循环。如果实际代码中误将setShow加了括号写成setTimeout(setShow(), 10000),会在渲染阶段立刻执行状态更新,页面一加载就会进入无限循环,和观察到的日志立刻无限打印的现象完全吻合。

修复方案

  1. 修复父组件无限循环
    将定时器逻辑移入useEffect,添加依赖和清理函数,避免渲染阶段重复注册定时器:
    // 替换原来的立即执行定时器代码
    useEffect(() => {
      if (alerts.length > 0) {
        const timer = setTimeout(() => {
          removeAllAlerts();
        }, 10000);
        // 组件卸载或依赖变化时清理已有定时器
        return () => clearTimeout(timer);
      }
    }, [alerts, removeAllAlerts]);
    
  2. 修复分页组件页码不显示
    不要用普通局部变量+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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 12:21:19