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

如何在Next.js中阻止子组件不必要的重渲染?

在Next.js中解决子组件不必要重渲染的方案

针对你遇到的Next.js(React)中子组件不必要重渲染问题,尤其在子到父路由场景下父组件重复渲染的情况,结合你提供的代码给出以下具体优化方案:

一、优化状态与渲染逻辑,减少父组件重触发

1. 合并重复的API请求逻辑

将多个独立的useEffect合并为一个,并行请求数据减少副作用执行次数,避免多次状态更新触发重渲染:

// 替换原有多个分散的useEffect
useEffect(() => {
  const fetchAllData = async () => {
    try {
      const [industriesData, practiseData, accountData] = await Promise.all([
        getIndustriesAPI(),
        getPracticesAPI(),
        getAccountsAPI()
      ]);
      
      setNames(industriesData.map(item => item.name));
      setPractise(practiseData.map(item => item.name));
      setAccount(accountData.map(item => item.accountType));
    } catch (error) {
      console.error('数据获取失败:', error);
    }
  };
  
  fetchAllData();
}, []);

2. 移除不必要的isMounted状态

"client"组件无需通过isMounted规避SSR问题,直接移除该状态及其相关逻辑,减少无意义的状态更新:

// 删除以下代码块
const [isMounted, setIsMounted] = useState(false);
useEffect(() => {
  setIsMounted(true);
}, []);

// 渲染部分直接去掉isMounted判断
return (
  <HeaderSideNav>
    {/* ...原有内容... */}
  </HeaderSideNav>
);

3. 缓存静态数据与配置

将columns、top100Films这类不会变化的变量放到组件外部,避免每次渲染时创建新对象导致子组件因props变化重渲染:

// 静态配置移至组件外部
const columns = [
  { label: '项目ID', accessor: 'projectId' },
  { label: '客户名称', accessor: 'clientName' },
  { label: '行业类型', accessor: 'industryType' },
  { label: '业务领域', accessor: 'practiceName' },
  { label: '提交人', accessor: 'submittedBy' },
  { label: '状态', accessor: 'status' },
];

const top100Films = [
  { label: '肖申克的救赎', year: 1994 },
  { label: '教父', year: 1972 },
  { label: '教父2', year: 1974 },
  { label: '黑暗骑士', year: 2008 },
  { label: '十二怒汉', year: 1957 },
  { label: '辛德勒的名单', year: 1993 },
  { label: '低俗小说', year: 1994 },
  { label: '指环王:王者归来', year: 2003 },
];

function Projects() {
  // ...组件内部逻辑...
}

二、用React.memo缓存子组件

给共享组件(如GlobalSearch、UseDropdown、UseTable)添加React.memo包裹,只有当props变化时才会触发重渲染:

// 以UseTable为例修改组件定义
import React, { memo } from 'react';

const UseTable = memo(({ columns, data }) => {
  // 组件原有逻辑
});

export default UseTable;

同理给GlobalSearch、UseDropdown等所有共享子组件做相同处理。

三、稳定回调函数引用

用useCallback包裹handleSearch这类回调函数,避免父组件每次渲染时创建新函数,导致子组件因props变化重渲染:

import { useCallback } from 'react';

function Projects() {
  // ...其他逻辑...
  const handleSearch = useCallback((query: string) => {
    console.log('搜索关键词:', query);
  }, []); // 依赖为空,函数引用保持稳定
  
  // ...渲染部分...
}

四、路由层面优化(子到父路由场景)

如果是嵌套路由导致父组件重复渲染,将HeaderSideNav放到layout.tsx中作为布局容器,页面作为子内容渲染,避免路由切换时重新挂载布局组件:

// app/Components/HeaderSideNav/layout.tsx
export default function Layout({ children }: { children: React.ReactNode }) {
  return <HeaderSideNav>{children}</HeaderSideNav>;
}

修改后Projects页面无需再嵌套HeaderSideNav,直接渲染业务内容即可,路由切换时仅更新子内容,布局组件保持挂载状态。

修改后的完整代码示例

"use client";
import React, { useEffect, useState, useCallback } from 'react';
import GlobalSearch from '@/app/Components/SharedComponents/GlobalSearch/page';
import UseDropdown from '@/app/Components/SharedComponents/UseDropdown/page';
import UseButton from '@/app/Components/SharedComponents/UseButton/page';
import UseTable from '@/app/Components/SharedComponents/UseTable/page';
import { Card, CardContent, Grid, SelectChangeEvent } from '@mui/material';
import { BreadcrumbsNav } from '@/app/Components/SharedComponents/BreadCrumbsNav/page';
import { Search as SearchIcon } from '@mui/icons-material';
import './Projects.css';
import { getAccountsAPI, getIndustriesAPI, getPracticesAPI, getProjectsByUserRoleAPI } from '@/app/Components/Services/page';
import ComboBox from '@/app/Components/SharedComponents/AutoComplete/page';
import { withAuth } from '@/app/authGuard';

// 静态配置
const columns = [
  { label: '项目ID', accessor: 'projectId' },
  { label: '客户名称', accessor: 'clientName' },
  { label: '行业类型', accessor: 'industryType' },
  { label: '业务领域', accessor: 'practiceName' },
  { label: '提交人', accessor: 'submittedBy' },
  { label: '状态', accessor: 'status' },
];

const top100Films = [
  { label: '肖申克的救赎', year: 1994 },
  { label: '教父', year: 1972 },
  { label: '教父2', year: 1974 },
  { label: '黑暗骑士', year: 2008 },
  { label: '十二怒汉', year: 1957 },
  { label: '辛德勒的名单', year: 1993 },
  { label: '低俗小说', year: 1994 },
  { label: '指环王:王者归来', year: 2003 },
];

interface Project {
  projectId: string;
  clientName: string;
  industryType: string;
  practiceName: string;
  submittedBy: string;
  role: string;
  projectName: string;
  accountType: string;
  status: string;
  technology: string;
  description: string;
  architecture: string;
  startDate: string;
  endDate: string;
}

function Projects() {
  const [selectedIndustry, setSelectedIndustry] = useState<string>('');
  const [names, setNames] = useState<string[]>([]);
  const [practise, setPractise] = useState<string[]>([]);
  const [account, setAccount] = useState<string[]>([]);
  const [projects, setProjects] = useState<Project[]>([]);

  // 合并API请求
  useEffect(() => {
    const fetchAllData = async () => {
      try {
        const [industriesData, practiseData, accountData] = await Promise.all([
          getIndustriesAPI(),
          getPracticesAPI(),
          getAccountsAPI()
        ]);
        setNames(industriesData.map(item => item.name));
        setPractise(practiseData.map(item => item.name));
        setAccount(accountData.map(item => item.accountType));
      } catch (error) {
        console.error('数据获取失败:', error);
      }
    };
    fetchAllData();
  }, []);

  // 获取项目数据
  useEffect(() => {
    const fetchProjects = async () => {
      try {
        const projects = await getProjectsByUserRoleAPI('Project Manager');
        setProjects(projects);
      } catch (error) {
        console.error('项目数据获取失败:', error);
      }
    };
    fetchProjects();
  }, []);

  // 稳定回调引用
  const handleSearch = useCallback((query: string) => {
    console.log('搜索关键词:', query);
  }, []);

  return (
    <>
      <BreadcrumbsNav
        links={[
          { name: "首页", to: "/Components/HeaderSideNav/Users/Projects" },
        ]}
        activeLink={"项目列表"}
      />
      <br />
      <GlobalSearch onSearch={handleSearch} />
      <br />
      <Card sx={{ boxShadow: "none" }}>
        <CardContent>
          <Grid container rowSpacing={2} columnSpacing={{ xs: 1, sm: 2, md: 3 }}>
            <Grid item xs={6}>
              <ComboBox options={top100Films} label={'项目名称'} />
            </Grid>
            <Grid item xs={6}>
              <UseDropdown
                label={"行业类型"}
                fullWidth
                options={names}
                onChange={(event) => setSelectedIndustry(event.target.value)}
                defaultValue={selectedIndustry}
              />
            </Grid>
            <Grid item xs={3}>
              <UseDropdown
                label={"账户类型"}
                fullWidth
                options={account}
                onChange={(event) => console.log(event.target.value)}
                defaultValue={''}
              />
            </Grid>
            <Grid item xs={3}>
              <UseDropdown
                label={"业务领域"}
                fullWidth
                options={practise}
                onChange={(event: SelectChangeEvent<string>) => {
                  // 补充实现逻辑
                }}
                defaultValue={''}
              />
            </Grid>
            <Grid item xs={3}>
              <UseButton variant='contained' color="primary" fullWidth className="custom-button">重置</UseButton>
            </Grid>
            <Grid item xs={3}>
              <UseButton variant='contained' color="primary" fullWidth style={{ height: "52px", backgroundColor: "#00aae7" }}>
                <SearchIcon /> 搜索
              </UseButton>
            </Grid>
          </Grid>
        </CardContent>
      </Card>
      <UseTable columns={columns} data={projects} />
    </>
  );
}

export default withAuth(Projects);

内容的提问来源于stack exchange,提问作者Yamuna Bolli

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.28 10:15:54