useQuery 的工作原理: 多次渲染问题处理
一次工作的渲染问题处理,第1次正常,第2次就缓存了上次的数据
🕐
解决方法1:
目前的改法,可以保持只渲染一次
核心代码 container
'use client';
import { useCreation, useUnmount } from 'ahooks';
import { ScopeProvider } from 'hooks/jotai';
import { notFound } from 'next/navigation';
import PageContainer from '@web/app/(pages)/components/PageContainer';
import { Loading } from '@webCommon/components/atoms/Loading';
import { DefaultWallpaper } from '@webCommon/components/molecules/Komi/DefaultWallpaper';
import { KomiWallpaper } from '@webCommon/components/molecules/Komi/KomiWallpaper';
import { useQuery, useQueryClient } from '@webCommon/hooks/useQuery';
import { getDuplicateEventConfigV2 } from '@webCommon/services/productEvent';
import { createEventDetailAtoms } from '../../../create/hooks/useEventSettings';
import { EventDuplicateForm } from './EventDuplicateForm';
export function EventDuplicatePageContainer({ eventId }: { eventId: string }) {
const queryClient = useQueryClient();
const queryKey = useCreation(() => {
return ['eventDuplicate', eventId];
}, [eventId]);
const [data, { loading, state }] = useQuery({
queryKey,
queryFn: () => getDuplicateEventConfigV2(eventId),
refetchOnWindowFocus: false,
retry: false,
});
/**
* 如果没有这里会有什么问题:
* 第1次: useQuery 取数据, loading 由 false -> true
* 第2次: useQuery 缓存里有值,会先将缓存里的值取出,然后 loading 表现为 false -> true -> true -> false
*/
useUnmount(() => {
queryClient.removeQueries({ queryKey });
});
if (state.isError) {
return notFound();
}
return (
<ScopeProvider atoms={createEventDetailAtoms}>
<PageContainer
disableContentPadding
pageLevel={1}
komiProps={{
startAdornment: (
<KomiWallpaper
fallback={<DefaultWallpaper />}
sx={{ backgroundColor: 'brand.white' }}
/>
),
Inner: { sx: { backgroundColor: 'brand.white' } },
}}
hideHeader
containerProps={{
className: 'pageContainerPlain',
sx: {
overscrollBehaviorX: 'none',
my: { xs: 0 },
width: '100%',
},
}}
>
{loading ? <Loading /> : <EventDuplicateForm event={data} />}
</PageContainer>
</ScopeProvider>
);
}子组件
'use client';
import { HydrateAtoms } from '@webCommon/atoms/components/HydrateAtoms';
import { type Event } from '@webCommon/services/catalog/event';
import { EventDetailForm } from '../../../create/forms/EventDetailForm';
import { eventDetailAtom, isDuplicateEventAtom } from '../../../create/hooks/useEventSettings';
export function EventDuplicateForm({ event }: { event: Event }) {
console.log('EventDuplicateForm rendered', {
eventId: event.id,
timestamp: Date.now(),
});
return (
<>
<HydrateAtoms
initialValues={[
[eventDetailAtom, event],
[isDuplicateEventAtom, true],
]}
/>
<EventDetailForm />
</>
);
}解决方法2:
父级继续2次渲染,子级继续更新
'use client';
import { HydrateAtoms } from '@webCommon/atoms/components/HydrateAtoms';
import { type Event } from '@webCommon/services/catalog/event';
import { EventDetailForm } from '../../../create/forms/EventDetailForm';
import { eventDetailAtom, isDuplicateEventAtom } from '../../../create/hooks/useEventSettings';
export function EventDuplicateForm({ event }: { event: Event }) {
return (
<>
<HydrateAtoms
initialValues={[
[eventDetailAtom, event],
[isDuplicateEventAtom, true],
]}
options={{ dangerouslyForceHydrate: true }}
/>
<EventDetailForm />
</>
);
}实际问题分析
# Duplicate Page 缓存优化方案
## 🚨 问题描述
在 duplicate 页面中,`useQuery` 会先显示缓存的上次数据,然后重新请求,导致:
1. **显示错误的旧数据**
2. **页面"闪烁"**(旧数据 → 新数据)
3. **多次渲染**(loading: false → true → false)
### 观察到的现象
```
第1次进入 duplicate 页面:
└─ 正常请求和显示
第2次进入 duplicate 页面(同一个事件):
├─ loading: false, data: 上次的缓存数据 ← 第1次渲染
├─ loading: true ← 第2次渲染(重新请求)
└─ loading: false, data: 新数据 ← 第3次渲染
```
## 🔍 根本原因
### React Query 的缓存机制
```typescript
const [data, { loading, state }] = useQuery({
queryKey: ['eventDuplicate', eventId],
queryFn: () => getDuplicateEventConfigV2(eventId),
refetchOnWindowFocus: false,
retry: false,
// 默认配置(隐式):
// refetchOnMount: true ← 挂载时重新获取
// staleTime: 0 ← 数据立即过期
// gcTime: 5分钟 ← 缓存保留5分钟
});
```
### 执行流程
**第1次 duplicate 事件 A**:
```
1. 检查缓存:没有
2. loading: true
3. 发起请求
4. 请求成功,缓存数据
5. loading: false, data: data_A
6. 用户离开页面
```
**第2次 duplicate 事件 A**:
```
1. 检查缓存:有 data_A
2. loading: false, data: data_A(缓存) ← 显示旧数据!
3. refetchOnMount: true → 重新请求
4. loading: true
5. 请求成功,更新缓存
6. loading: false, data: data_A_new
```
### 为什么会这样?
这是 React Query 的 **"stale-while-revalidate"** 策略:
1. **快速显示内容**:先显示缓存数据(虽然是旧的)
2. **后台更新**:同时在后台重新请求最新数据
3. **更新显示**:新数据返回后更新界面
**好处**:用户立即看到内容(虽然是旧的)
**坏处**:在 duplicate 场景中,旧数据是错误的(上次的值)
## ✅ 解决方案
### 使用 `useUnmount` 清除缓存
```typescript
import { useUnmount } from 'ahooks';
import { useQueryClient } from '@hooks/jotai';
export function EventDuplicatePageContainer({ eventId }: { eventId: string }) {
const queryClient = useQueryClient();
// 离开页面时清除缓存,避免下次进入时显示旧的缓存数据
useUnmount(() => {
queryClient.removeQueries({ queryKey: ['eventDuplicate', eventId] });
});
const [data, { loading, state }] = useQuery({
queryKey: ['eventDuplicate', eventId],
queryFn: () => getDuplicateEventConfigV2(eventId),
refetchOnWindowFocus: false,
retry: false,
});
// ...
}
```
## 📊 优化效果
### 优化前
| 次数 | 缓存状态 | loading | data | 渲染次数 | 问题 |
|------|---------|---------|------|---------|------|
| 第1次进入 | 无 | false→true→false | undefined→新数据 | 3次 | 正常 |
| 第2次进入 | 有(旧) | false→true→false | 旧数据→新数据 | 3次 | ❌ 显示旧数据 |
### 优化后
| 次数 | 缓存状态 | loading | data | 渲染次数 | 效果 |
|------|---------|---------|------|---------|------|
| 第1次进入 | 无 | true→false | undefined→新数据 | 2次 | ✅ 正常 |
| 离开页面 | - | - | - | - | ✅ 清除缓存 |
| 第2次进入 | 无(已清除) | true→false | undefined→新数据 | 2次 | ✅ 只显示新数据 |
## 🎯 关键点
### 1. 为什么用 `useUnmount` 而不是 `useEffect`?
**useUnmount**(推荐):
```typescript
useUnmount(() => {
queryClient.removeQueries({ queryKey: ['eventDuplicate', eventId] });
});
```
- ✅ **语义清晰**:专门用于卸载时的清理
- ✅ **简洁**:不需要 return cleanup 函数
- ✅ **可读性好**:一眼看出是卸载时的操作
**useEffect**(也可以):
```typescript
useEffect(() => {
return () => {
queryClient.removeQueries({ queryKey: ['eventDuplicate', eventId] });
};
}, [queryClient, eventId]);
```
- ✅ 功能相同
- ⚠️ 需要依赖数组
- ⚠️ 可读性稍差
### 2. 为什么不直接设置 `refetchOnMount: false`?
```typescript
// ❌ 不推荐
const [data, { loading, state }] = useQuery({
refetchOnMount: false, // 禁用挂载时重新获取
});
```
**问题**:
- ⚠️ 如果数据真的更新了,不会获取最新数据
- ⚠️ 用户可能看到过时的数据
- ⚠️ 不符合 duplicate 页面的需求(每次都要最新数据)
### 3. 性能影响
**网络请求**:
- ✅ 每次都重新请求(1次)
- ✅ 不会显示旧数据
- ✅ 用户体验更好
**对比**:
- 方案A(不清除缓存):可能显示旧数据,然后重新请求(2次渲染)
- 方案B(清除缓存):直接请求新数据(1次渲染)✅
## 💡 其他方案对比
| 方案 | 优点 | 缺点 | 推荐度 |
|------|------|------|--------|
| **useUnmount 清除缓存** | ✅ 简洁<br>✅ 语义清晰<br>✅ 只渲染1次 | ⚠️ 每次都请求 | ⭐⭐⭐⭐⭐ |
| refetchOnMount: false | ✅ 不重新请求 | ❌ 可能显示过时数据 | ⭐⭐ |
| staleTime: 0 | ✅ 数据立即过期 | ❌ 还是会先显示缓存 | ⭐⭐ |
| gcTime: 0 | ✅ 不保留缓存 | ❌ 影响其他页面 | ⭐ |
## 📝 完整代码
```typescript
'use client';
import { useUnmount } from 'ahooks';
import { useQueryClient } from '@hooks/jotai';
import { ScopeProvider } from 'hooks/jotai';
import { notFound } from 'next/navigation';
import PageContainer from '@web/app/(pages)/components/PageContainer';
import { Loading } from '@webCommon/components/atoms/Loading';
import { DefaultWallpaper } from '@webCommon/components/molecules/Komi/DefaultWallpaper';
import { KomiWallpaper } from '@webCommon/components/molecules/Komi/KomiWallpaper';
import { useQuery } from '@webCommon/hooks/useQuery';
import { getDuplicateEventConfigV2 } from '@webCommon/services/productEvent';
import { createEventDetailAtoms } from '../../../create/hooks/useEventSettings';
import { EventDuplicateForm } from './EventDuplicateForm';
export function EventDuplicatePageContainer({ eventId }: { eventId: string }) {
const queryClient = useQueryClient();
// 离开页面时清除缓存,避免下次进入时显示旧的缓存数据
useUnmount(() => {
queryClient.removeQueries({ queryKey: ['eventDuplicate', eventId] });
});
const [data, { loading, state }] = useQuery({
queryKey: ['eventDuplicate', eventId],
queryFn: () => getDuplicateEventConfigV2(eventId),
refetchOnWindowFocus: false,
retry: false,
});
if (state.isError) {
return notFound();
}
return (
<ScopeProvider atoms={createEventDetailAtoms}>
<PageContainer
disableContentPadding
pageLevel={1}
komiProps={{
startAdornment: (
<KomiWallpaper
fallback={<DefaultWallpaper />}
sx={{ backgroundColor: 'brand.white' }}
/>
),
Inner: { sx: { backgroundColor: 'brand.white' } },
}}
hideHeader
containerProps={{
className: 'pageContainerPlain',
sx: {
overscrollBehaviorX: 'none',
my: { xs: 0 },
width: '100%',
},
}}
>
{loading ? <Loading /> : <EventDuplicateForm event={data} />}
</PageContainer>
</ScopeProvider>
);
}
```
## 🎯 总结
### 问题
React Query 的缓存机制导致第2次进入时先显示旧数据,然后重新请求。
### 解决方案
使用 `useUnmount` 在离开页面时清除缓存。
### 效果
- ✅ 不会显示旧数据
- ✅ 只渲染1次
- ✅ 用户体验更好
- ✅ 代码简洁清晰
---
**优化时间**: 2026-03-05
**优化者**: Claude Code Analysis Tool