在 Next.js(特别是 App Router 和 Pages Router 都适用)中,触发组件重新渲染的方式主要可以分为两大类:
'use client' 的组件)→ 跟普通 React 几乎一样在 Next.js(特别是 App Router,Next.js 13+ / 14+ / 15 的主流做法)中,Client Component 和 Server Component 的重新渲染(re-render / 重新取资料 / 重新产生 UI)机制完全不同。
下面把两边最常见的触发方式分开整理对比:
'use client' 的组件)跟普通 React 几乎一样,主要靠 React 自身的更新机制
| 触发方式 | 说明 | 常见使用场景 | 频率/推荐度 |
|---|---|---|---|
setState / useState setter | 最经典、最常用的方式 | 任何用户交互、表单、开关、计数器等 | ★★★★★ |
dispatch(useReducer) | 适合复杂状态逻辑 | 多步骤表单、购物车、游戏状态等 | ★★★★ |
| 父组件的 props 改变 | 父组件重新渲染 → 子组件跟着变 | 列表项、条件渲染、从上层传数据 | ★★★★ |
| Context 值改变 | Provider 值变 → 所有消费该 context 的组件重渲染 | 主题、全局用户、语言切换 | ★★★ |
改变组件的 key 属性 | React 认为「全新组件」,强制完全重新 mount | 重置表单、强制重跑第三方库、动画重置 | ★★★ |
useEffect 里修改 state | 副作用 → state → 再渲染(小心无限循环) | 轮询、监听某些变化后更新状态 | ★★△ |
forceUpdate()(类组件) | 几乎没人用了 | 极少数遗留类组件 | ★☆ |
| 外部事件/第三方库引起更新 | 如某些 zustand、jotai、redux 等状态库的更新 | 全局状态管理场景 | 视情况 |
小结:Client Component 的重新渲染基本上就是「React 认为需要更新」的时候,跟 Next.js 本身的路由/缓存机制关系不大。
Server Component 本身没有状态,也没有传统意义上的「re-render」概念。 它每次都是全新执行一次(像函数重新跑一次),所以要让它「看起来重新渲染」,其实是要让它重新被调用并产生新结果。
| 触发方式 | 说明 | 适用场景 | 推荐度 |
|---|---|---|---|
router.refresh() | 从客户端最简单粗暴的方式,让当前路由重新请求 Server Components | 表单提交后、乐观更新后想立刻看到最新数据 | ★★★★★ |
revalidatePath(path) | 标记某个路径的缓存失效,下次访问时重新生成 | CRUD 后让列表页/详情页更新 | ★★★★ |
revalidateTag(tag) | 更细粒度的缓存失效(推荐),按 tag 失效 | 多个页面共用同一份数据时 | ★★★★☆ |
| Server Action 执行后自动 revalidate | 在 Server Action 里调用上面任一 revalidate 方法 | 表单提交、删除、点赞等 mutation 操作 | ★★★★ |
| 更改路由段配置强制动态渲染 | export const dynamic = 'force-dynamic' 或 fetch 用 cache: 'no-store' | 需要每次都拿最新数据的页面(如仪表盘) | ★★★ |
dynamicParams = false + 重新生成 | 结合 revalidate 后重新访问动态路由参数 | 静态 + 按需增量静态再生 | ★★△ |
| 浏览器硬刷新 / 重新打开页面 | 最暴力方式,绕过所有缓存 | 开发调试、用户手动刷新 | ★☆ |
重要补充说明(2025-2026 主流认知):
router.refresh() 是目前最常用的客户端触发 Server Component 更新的方式
→ 它会保留 Client Component 的状态(useState 不会丢失),只重新拉取 Server Component 部分revalidateTag 是目前最推荐的精细控制组合
→ 性能最好、范围最可控{BarChart()} 这种样子,有时候写成 <BarChart /> 这种样子?在 Next.js(或任何 React 项目)中,你看到组件两种写法:
x1{ BarChart() } ← 像调用函数一样23<BarChart /> ← 像 JSX 标签一样这两种写法本质上代表完全不同的调用方式,而且区别非常重要,下面用最直白的方式解释:
| 写法 | 实际执行的是什么 | 返回值类型 | 什么时候用 | 常见使用场景 | 是否会触发 React 的渲染机制 |
|---|---|---|---|---|---|
{ BarChart() } | 普通函数调用 | 任何类型(通常是 JSX 或字符串) | 当你把组件当成普通函数使用时 | 返回 JSX 片段、字符串、数字、条件渲染等 | 不会自动触发 React 渲染(只是值) |
<BarChart /> | React.createElement 调用 | React Element(虚拟 DOM 节点) | 当你想让 React 管理这个组件的渲染、状态、生命周期时 | 绝大多数正常组件使用场景 | 会,React 会跟踪、diff、更新它 |
{ BarChart() } → 只是调用函数,拿返回值
xxxxxxxxxx151function BarChart() {2 return <div>这是一个柱状图</div>;3}45// 使用方式 1:像函数调用6function Page() {7 const chart = BarChart(); // ← 只是执行一次,返回 JSX89 return (10 <div>11 <h1>标题</h1>12 {chart} // ← 这里只是把刚才拿到的 JSX 放进去13 </div>14 );15}常见真实场景:
xxxxxxxxxx81function getChart(type: 'bar' | 'line') {2 if (type === 'bar') return BarChart();3 if (type === 'line') return LineChart();4 return <div>暂无图表</div>;5}67// 使用8{getChart('bar')}<BarChart /> → 告诉 React:这是一个组件实例,请管理它
xxxxxxxxxx81function Page() {2 return (3 <div>4 <h1>标题</h1>5 <BarChart /> // ← React 会创建组件实例,管理状态、effect 等6 </div>7 );8}这种写法才是大多数组件的正常用法,因为:
useState、useEffect 等 hooks 时必须用这种方式举个典型例子:
xxxxxxxxxx61function BarChart({ data }: { data: number[] }) {2 const [count, setCount] = useState(0); // ← 这里有状态,必须用 <BarChart />3 useEffect(() => { /* ... */ }, [data]);45 return <div>柱状图:{count}</div>;6}如果你写成 { BarChart({ data }) },虽然能跑,但 hooks 会报错(因为违反了 hooks 规则:只能在 React 组件或自定义 hook 里调用)。
{ BarChart() } → 你只是借用这个函数来产生 JSX 片段,不需要 React 管理它的生命周期和状态(像工具函数)<BarChart /> → 你告诉 React:这是个正式组件,请跟踪它的状态、props 变化、重新渲染等(99% 的情况用这个)