第02章:AI帮我用了useState但没有处理异步状态
第02章:AI帮我用了useState但没有处理异步状态
“AI 给你写了一个带有 loading/error/data 三个 useState 的数据获取组件。看起来很完整,但仔细一想:loading 设为 true,然后 await fetch,然后 loading 设为 false——如果组件在请求途中被卸载了呢?如果两个请求同时在飞,先发的后回来了呢?如果请求出错,error 设了,但 loading 没有置为 false 呢?这些竞态条件(Race Condition)AI 几乎不会主动提醒你。”
ℹ️ 版本说明:本章基于 React 19.2.6。
2.1 AI默认会生成什么
// AI 通常给你的代码(基础但有竞态问题)
function SearchResults({ query }) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
fetch(`/api/search?q=${query}`)
.then(res => res.json())
.then(setData) // ← 可能更新已卸载的组件!
.catch(setError)
.finally(() => setLoading(false));
}, [query]);
if (loading) return <Spinner />;
if (error) return <div>Error: {error.message}</div>;
return <div>{data?.results?.map(r => <Item key={r.id} {...r} />)}</div>;
}
2.2 AI通常遗漏的4个坑
⚠️ 坑1:组件卸载后设置 state(内存泄漏警告)
// 问题:请求飞出去了,但用户已经切换页面,组件已卸载
// 请求回来后还调用 setState,React 会警告(虽然 React 18+ 不会内存泄漏,但逻辑错误)
useEffect(() => {
fetch('/api/data').then(data => {
setData(data); // 组件已卸载!无意义的状态更新
});
}, []);
// 修复:用 AbortController 取消请求
useEffect(() => {
const controller = new AbortController();
fetch('/api/data', { signal: controller.signal })
.then(res => res.json())
.then(setData)
.catch(err => {
if (err.name !== 'AbortError') setError(err); // 忽略取消引起的错误
});
return () => controller.abort(); // cleanup:组件卸载时取消请求
}, []);
⚠️ 坑2:竞态条件(先发的请求后回来)
// 问题:用户快速输入 "a" → "ab" → "abc"
// 发出了3个请求,但 "a" 的响应可能在 "abc" 之后回来
// 最终展示的是 "a" 的结果,而用户看的是 "abc"!
useEffect(() => {
fetch(`/api/search?q=${query}`)
.then(res => res.json())
.then(setData); // ← 不知道这是第几个请求的结果
}, [query]);
// 修复方案1:用 AbortController 取消过期请求
useEffect(() => {
const controller = new AbortController();
fetch(`/api/search?q=${query}`, { signal: controller.signal })
.then(res => res.json())
.then(setData)
.catch(err => { if (err.name !== 'AbortError') setError(err); });
return () => controller.abort(); // 新请求来时,取消旧请求
}, [query]);
// 修复方案2:版本号(ignore flag)
useEffect(() => {
let current = true; // 是否是最新的请求
fetch(`/api/search?q=${query}`)
.then(res => res.json())
.then(data => {
if (current) setData(data); // 只有最新的请求才更新状态
});
return () => { current = false; }; // 旧请求返回时 current 已是 false
}, [query]);
⚠️ 坑3:error 时 loading 没有置为 false
// 问题:常见的写法(Promise 链)
useEffect(() => {
setLoading(true);
fetch('/api/data')
.then(res => res.json())
.then(data => {
setData(data);
setLoading(false); // 只有成功才置为 false
})
.catch(err => {
setError(err);
// ← 忘记 setLoading(false)!错误后页面永远转圈
});
}, []);
// 修复:finally 总是执行
fetch('/api/data')
.then(res => res.json())
.then(setData)
.catch(setError)
.finally(() => setLoading(false)); // ← 无论成功失败都置为 false
⚠️ 坑4:React 19 建议:用 useActionState 或 use() 替代手写异步状态
// React 19 之前:手写 loading/error/data(容易出错)
const [data, setData] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
// React 19 方案A:use() hook(数据获取)
// 不再需要 loading/error state!由 Suspense/ErrorBoundary 处理
const data = use(fetchData(id));
// React 19 方案B:useActionState(表单/用户触发的操作)
const [state, submitAction, isPending] = useActionState(
async (prevState, formData) => {
try {
const result = await submitForm(formData);
return { data: result, error: null };
} catch (err) {
return { data: null, error: err.message };
}
},
{ data: null, error: null }
);
// isPending 自动管理 loading 状态!不需要手写
2.3 更好的提示词
提示词 P01:修复竞态条件
使用时机:数据获取偶尔显示错误的结果,快速切换参数时尤其明显
帮我修复 React 19.2.6 组件的数据获取竞态条件。
当前代码:
[粘贴组件代码]
问题现象:
[快速切换 query/id 等参数时,偶尔展示的是旧参数的数据]
需要:
1. 用 AbortController 取消过期请求(推荐方案)
2. 修复 error 时 loading 没有置为 false 的问题(如果有)
3. 修复组件卸载后 setState 的问题(如果有)
可选:
- 如果这是一个搜索场景(用户输入),加上 debounce(300ms)
- 如果可以改造成 use() hook,给我展示新写法对比
基于 React 19.2.6。
提示词 P02:用 useActionState 管理用户操作状态
使用时机:按钮点击、表单提交等用户触发的异步操作
帮我用 React 19.2.6 的 useActionState 重写这个组件的异步操作。
当前代码(手写 loading/error state):
[粘贴代码]
操作类型:[表单提交 / 按钮点击 / 批量删除]
需要:
1. useActionState 的基本用法:
const [state, submitAction, isPending] = useActionState(action, initialState)
- state: 上次操作的结果(成功/失败的数据)
- submitAction: 触发操作的函数(可以直接作为 form action)
- isPending: 是否正在执行(自动管理!)
2. 乐观更新(如果需要):配合 useOptimistic(第5章)
3. Server Actions(如果是 Next.js):
- action 函数如何标记为 'use server'
- 如何在 form 里直接使用 Server Action
4. 错误处理:如何在 state 里区分成功和失败状态
给我完整的重写代码。
基于 React 19.2.6 + [Next.js 15.x / Vite]。
提示词 P03:自定义数据获取 Hook(带取消和防抖)
使用时机:多个地方需要相同的数据获取逻辑
帮我写一个 React 19.2.6 自定义 Hook,用于数据获取,包含:
功能需求:
1. 参数变化时自动重新请求(带竞态保护)
2. 组件卸载时自动取消请求(AbortController)
3. 可选:防抖(适合搜索场景)
4. 返回:{ data, loading, error, refetch }
使用场景:
- 搜索(防抖300ms + 竞态保护)
- 详情页(参数变化时重新加载)
- 列表页(支持手动 refetch)
Hook 签名(参考):
const { data, loading, error, refetch } = useFetch('/api/users', {
params: { page, search },
debounce: 300, // 可选
enabled: !!search, // 条件触发
});
注意:
- 也要展示 React 19 use() hook 方案的对比
- 什么情况下用自定义 Hook,什么情况下用 use()?
基于 React 19.2.6。
2.4 验收清单
| 检查项 | 验证方法 | AI辅助 |
|---|---|---|
| 所有 fetch 都有 AbortController | 代码里有 controller.abort() cleanup |
用 P01 修复 |
| error 时 loading 置为 false | 模拟网络错误,验证 spinner 消失 | 用 P01 修复 |
| 竞态保护:快速切换参数 | DevTools Network 旧请求已取消(canceled) | 用 P01 修复 |
| 用户操作用 useActionState | 代码里有 useActionState,无手写 loading | 用 P02 改造 |
| 数据获取考虑用 use() hook | 新组件用 use() 而不是 useEffect | 用 P02 对比 |
| 有 Suspense + ErrorBoundary 包裹 | use() 组件外有对应的边界处理 | 第9章 |
2.5 本章小结
如果你只记一件事:所有用 useEffect 发起请求的地方,都要加 cleanup 函数取消请求:return () => controller.abort()。这一行代码解决了竞态条件和组件卸载后 setState 两个问题。
异步状态管理的三个层次:
- 取消和竞态保护(AbortController + cleanup):每个 useEffect 里的 fetch 都要有取消逻辑,防止旧请求覆盖新结果
- React 19 新 API(useActionState + use()):用户触发的操作用 useActionState 省去手写 loading/error,数据获取用 use() hook
- 封装成 Hook 或用库(useFetch / React Query / SWR):项目里有多个数据获取场景时,统一封装,不要在每个组件里重复写 AbortController
→ 第3章:AI不知道什么时候用Server Components