如何在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
相关产品推荐
相关产品推荐

