# React Code Review Guide React 审查重点:Hooks 规则、性能优化的适度性、组件设计、以及现代 React 19/RSC 模式。 ## 目录 - [基础 Hooks 规则](#基础-hooks-规则) - [useEffect 模式](#useeffect-模式) - [useMemo / useCallback](#usememo--usecallback) - [组件设计](#组件设计) - [Error Boundaries & Suspense](#error-boundaries--suspense) - [Server Components (RSC)](#server-components-rsc) - [React 19 Actions & Forms](#react-19-actions--forms) - [Suspense & Streaming SSR](#suspense--streaming-ssr) - [TanStack Query v5](#tanstack-query-v5) - [Review Checklists](#review-checklists) --- ## 基础 Hooks 规则 ```tsx // ❌ 条件调用 Hooks — 违反 Hooks 规则 function BadComponent({ isLoggedIn }) { if (isLoggedIn) { const [user, setUser] = useState(null); // Error! } return
...
; } // ✅ Hooks 必须在组件顶层调用 function GoodComponent({ isLoggedIn }) { const [user, setUser] = useState(null); if (!isLoggedIn) return ; return
{user?.name}
; } ``` --- ## useEffect 模式 ```tsx // ❌ 依赖数组缺失或不完整 function BadEffect({ userId }) { const [user, setUser] = useState(null); useEffect(() => { fetchUser(userId).then(setUser); }, []); // 缺少 userId 依赖! } // ✅ 完整的依赖数组 function GoodEffect({ userId }) { const [user, setUser] = useState(null); useEffect(() => { let cancelled = false; fetchUser(userId).then(data => { if (!cancelled) setUser(data); }); return () => { cancelled = true; }; // 清理函数 }, [userId]); } // ❌ useEffect 用于派生状态(反模式) function BadDerived({ items }) { const [filteredItems, setFilteredItems] = useState([]); useEffect(() => { setFilteredItems(items.filter(i => i.active)); }, [items]); // 不必要的 effect + 额外渲染 return ; } // ✅ 直接在渲染时计算,或用 useMemo function GoodDerived({ items }) { const filteredItems = useMemo( () => items.filter(i => i.active), [items] ); return ; } // ❌ useEffect 用于事件响应 function BadEventEffect() { const [query, setQuery] = useState(''); useEffect(() => { if (query) { analytics.track('search', { query }); // 应该在事件处理器中 } }, [query]); } // ✅ 在事件处理器中执行副作用 function GoodEvent() { const [query, setQuery] = useState(''); const handleSearch = (q: string) => { setQuery(q); analytics.track('search', { query: q }); }; } ``` --- ## useMemo / useCallback ```tsx // ❌ 过度优化 — 常量不需要 useMemo function OverOptimized() { const config = useMemo(() => ({ timeout: 5000 }), []); // 无意义 const handleClick = useCallback(() => { console.log('clicked'); }, []); // 如果不传给 memo 组件,无意义 } // ✅ 只在需要时优化 function ProperlyOptimized() { const config = { timeout: 5000 }; // 简单对象直接定义 const handleClick = () => console.log('clicked'); } // ❌ useCallback 依赖总是变化 function BadCallback({ data }) { // data 每次渲染都是新对象,useCallback 无效 const process = useCallback(() => { return data.map(transform); }, [data]); } // ✅ useMemo + useCallback 配合 React.memo 使用 const MemoizedChild = React.memo(function Child({ onClick, items }) { return
{items.length}
; }); function Parent({ rawItems }) { const items = useMemo(() => processItems(rawItems), [rawItems]); const handleClick = useCallback(() => { console.log(items.length); }, [items]); return ; } ``` --- ## 组件设计 ```tsx // ❌ 在组件内定义组件 — 每次渲染都创建新组件 function BadParent() { function ChildComponent() { // 每次渲染都是新函数! return
child
; } return ; } // ✅ 组件定义在外部 function ChildComponent() { return
child
; } function GoodParent() { return ; } // ❌ Props 总是新对象引用 function BadProps() { return ( {}} // 每次渲染新函数 /> ); } // ✅ 稳定的引用 const style = { color: 'red' }; function GoodProps() { const handleClick = useCallback(() => {}, []); return ; } ``` --- ## Error Boundaries & Suspense ```tsx // ❌ 没有错误边界 function BadApp() { return ( }> {/* 错误会导致整个应用崩溃 */} ); } // ✅ Error Boundary 包裹 Suspense function GoodApp() { return ( }> }> ); } ``` --- ## Server Components (RSC) ```tsx // ❌ 在 Server Component 中使用客户端特性 // app/page.tsx (Server Component by default) function BadServerComponent() { const [count, setCount] = useState(0); // Error! No hooks in RSC return ; } // ✅ 交互逻辑提取到 Client Component // app/counter.tsx 'use client'; function Counter() { const [count, setCount] = useState(0); return ; } // app/page.tsx (Server Component) async function GoodServerComponent() { const data = await fetchData(); // 可以直接 await return (

{data.title}

{/* 客户端组件 */}
); } // ❌ 'use client' 放置不当 — 整个树都变成客户端 // layout.tsx 'use client'; // 这会让所有子组件都成为客户端组件 export default function Layout({ children }) { ... } // ✅ 只在需要交互的组件使用 'use client' // 将客户端逻辑隔离到叶子组件 ``` --- ## React 19 Actions & Forms React 19 引入了 Actions 系统和新的表单处理 Hooks,简化异步操作和乐观更新。 ### useActionState ```tsx // ❌ 传统方式:多个状态变量 function OldForm() { const [isPending, setIsPending] = useState(false); const [error, setError] = useState(null); const [data, setData] = useState(null); const handleSubmit = async (formData: FormData) => { setIsPending(true); setError(null); try { const result = await submitForm(formData); setData(result); } catch (e) { setError(e.message); } finally { setIsPending(false); } }; } // ✅ React 19: useActionState 统一管理 import { useActionState } from 'react'; function NewForm() { const [state, formAction, isPending] = useActionState( async (prevState, formData: FormData) => { try { const result = await submitForm(formData); return { success: true, data: result }; } catch (e) { return { success: false, error: e.message }; } }, { success: false, data: null, error: null } ); return (
{state.error &&

{state.error}

}
); } ``` ### useFormStatus ```tsx // ❌ Props 透传表单状态 function BadSubmitButton({ isSubmitting }) { return ; } // ✅ useFormStatus 访问父
状态(无需 props) import { useFormStatus } from 'react-dom'; function SubmitButton() { const { pending, data, method, action } = useFormStatus(); // 注意:必须在 内部的子组件中使用 return ( ); } // ❌ useFormStatus 在 form 同级组件中调用——不工作 function BadForm() { const { pending } = useFormStatus(); // 这里无法获取状态! return (
); } // ✅ useFormStatus 必须在 form 的子组件中 function GoodForm() { return (
{/* useFormStatus 在这里面调用 */} ); } ``` ### useOptimistic ```tsx // ❌ 等待服务器响应再更新 UI function SlowLike({ postId, likes }) { const [likeCount, setLikeCount] = useState(likes); const [isPending, setIsPending] = useState(false); const handleLike = async () => { setIsPending(true); const newCount = await likePost(postId); // 等待... setLikeCount(newCount); setIsPending(false); }; } // ✅ useOptimistic 即时反馈,失败自动回滚 import { useOptimistic } from 'react'; function FastLike({ postId, likes }) { const [optimisticLikes, addOptimisticLike] = useOptimistic( likes, (currentLikes, increment: number) => currentLikes + increment ); const handleLike = async () => { addOptimisticLike(1); // 立即更新 UI try { await likePost(postId); // 后台同步 } catch { // React 自动回滚到 likes 原值 } }; return ; } ``` ### Server Actions (Next.js 15+) ```tsx // ❌ 客户端调用 API 'use client'; function ClientForm() { const handleSubmit = async (formData: FormData) => { const res = await fetch('/api/submit', { method: 'POST', body: formData, }); // ... }; } // ✅ Server Action + useActionState // actions.ts 'use server'; export async function createPost(prevState: any, formData: FormData) { const title = formData.get('title'); await db.posts.create({ title }); revalidatePath('/posts'); return { success: true }; } // form.tsx 'use client'; import { createPost } from './actions'; function PostForm() { const [state, formAction, isPending] = useActionState(createPost, null); return (
); } ``` --- ## Suspense & Streaming SSR Suspense 和 Streaming 是 React 18+ 的核心特性,在 2025 年的 Next.js 15 等框架中广泛使用。 ### 基础 Suspense ```tsx // ❌ 传统加载状态管理 function OldComponent() { const [data, setData] = useState(null); const [isLoading, setIsLoading] = useState(true); useEffect(() => { fetchData().then(setData).finally(() => setIsLoading(false)); }, []); if (isLoading) return ; return ; } // ✅ Suspense 声明式加载状态 function NewComponent() { return ( }> {/* 内部使用 use() 或支持 Suspense 的数据获取 */} ); } ``` ### 多个独立 Suspense 边界 ```tsx // ❌ 单一边界——所有内容一起加载 function BadLayout() { return ( }>
{/* 慢 */} {/* 快 */} ); } // ✅ 独立边界——各部分独立流式传输 function GoodLayout() { return ( <>
{/* 立即显示 */}
}> {/* 独立加载 */} }> {/* 独立加载 */}
); } ``` ### Next.js 15 Streaming ```tsx // app/page.tsx - 自动 Streaming export default async function Page() { // 这个 await 不会阻塞整个页面 const data = await fetchSlowData(); return
{data}
; } // app/loading.tsx - 自动 Suspense 边界 export default function Loading() { return ; } ``` ### use() Hook (React 19) ```tsx // ✅ 在组件中读取 Promise import { use } from 'react'; function Comments({ commentsPromise }) { const comments = use(commentsPromise); // 自动触发 Suspense return (
    {comments.map(c =>
  • {c.text}
  • )}
); } // 父组件创建 Promise,子组件消费 function Post({ postId }) { const commentsPromise = fetchComments(postId); // 不 await return (
}>
); } ``` --- ## TanStack Query v5 TanStack Query 是 React 生态中最流行的数据获取库,v5 是当前稳定版本。 ### 基础配置 ```tsx // ❌ 不正确的默认配置 const queryClient = new QueryClient(); // 默认配置可能不适合 // ✅ 生产环境推荐配置 const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 1000 * 60 * 5, // 5 分钟内数据视为新鲜 gcTime: 1000 * 60 * 30, // 30 分钟后垃圾回收(v5 重命名) retry: 3, refetchOnWindowFocus: false, // 根据需求决定 }, }, }); ``` ### queryOptions (v5 新增) ```tsx // ❌ 重复定义 queryKey 和 queryFn function Component1() { const { data } = useQuery({ queryKey: ['users', userId], queryFn: () => fetchUser(userId), }); } function prefetchUser(queryClient, userId) { queryClient.prefetchQuery({ queryKey: ['users', userId], // 重复! queryFn: () => fetchUser(userId), // 重复! }); } // ✅ queryOptions 统一定义,类型安全 import { queryOptions } from '@tanstack/react-query'; const userQueryOptions = (userId: string) => queryOptions({ queryKey: ['users', userId], queryFn: () => fetchUser(userId), }); function Component1({ userId }) { const { data } = useQuery(userQueryOptions(userId)); } function prefetchUser(queryClient, userId) { queryClient.prefetchQuery(userQueryOptions(userId)); } // getQueryData 也是类型安全的 const user = queryClient.getQueryData(userQueryOptions(userId).queryKey); ``` ### 常见陷阱 ```tsx // ❌ staleTime 为 0 导致过度请求 useQuery({ queryKey: ['data'], queryFn: fetchData, // staleTime 默认为 0,每次组件挂载都会 refetch }); // ✅ 设置合理的 staleTime useQuery({ queryKey: ['data'], queryFn: fetchData, staleTime: 1000 * 60, // 1 分钟内不会重新请求 }); // ❌ 在 queryFn 中使用不稳定的引用 function BadQuery({ filters }) { useQuery({ queryKey: ['items'], // queryKey 没有包含 filters! queryFn: () => fetchItems(filters), // filters 变化不会触发重新请求 }); } // ✅ queryKey 包含所有影响数据的参数 function GoodQuery({ filters }) { useQuery({ queryKey: ['items', filters], // filters 是 queryKey 的一部分 queryFn: () => fetchItems(filters), }); } ``` ### useSuspenseQuery > **重要限制**:useSuspenseQuery 与 useQuery 有显著差异,选择前需了解其限制。 #### useSuspenseQuery 的限制 | 特性 | useQuery | useSuspenseQuery | |------|----------|------------------| | `enabled` 选项 | ✅ 支持 | ❌ 不支持 | | `placeholderData` | ✅ 支持 | ❌ 不支持 | | `data` 类型 | `T \| undefined` | `T`(保证有值)| | 错误处理 | `error` 属性 | 抛出到 Error Boundary | | 加载状态 | `isLoading` 属性 | 挂起到 Suspense | #### 不支持 enabled 的替代方案 ```tsx // ❌ 使用 useQuery + enabled 实现条件查询 function BadSuspenseQuery({ userId }) { const { data } = useSuspenseQuery({ queryKey: ['user', userId], queryFn: () => fetchUser(userId), enabled: !!userId, // useSuspenseQuery 不支持 enabled! }); } // ✅ 组件组合实现条件渲染 function GoodSuspenseQuery({ userId }) { // useSuspenseQuery 保证 data 是 T 不是 T | undefined const { data } = useSuspenseQuery({ queryKey: ['user', userId], queryFn: () => fetchUser(userId), }); return ; } function Parent({ userId }) { if (!userId) return ; return ( }> ); } ``` #### 错误处理差异 ```tsx // ❌ useSuspenseQuery 没有 error 属性 function BadErrorHandling() { const { data, error } = useSuspenseQuery({...}); if (error) return ; // error 总是 null! } // ✅ 使用 Error Boundary 处理错误 function GoodErrorHandling() { return ( }> }> ); } function DataComponent() { // 错误会抛出到 Error Boundary const { data } = useSuspenseQuery({ queryKey: ['data'], queryFn: fetchData, }); return ; } ``` #### 何时选择 useSuspenseQuery ```tsx // ✅ 适合场景: // 1. 数据总是需要的(无条件查询) // 2. 组件必须有数据才能渲染 // 3. 使用 React 19 的 Suspense 模式 // 4. 服务端组件 + 客户端 hydration // ❌ 不适合场景: // 1. 条件查询(根据用户操作触发) // 2. 需要 placeholderData 或初始数据 // 3. 需要在组件内处理 loading/error 状态 // 4. 多个查询有依赖关系 // ✅ 多个独立查询用 useSuspenseQueries function MultipleQueries({ userId }) { const [userQuery, postsQuery] = useSuspenseQueries({ queries: [ { queryKey: ['user', userId], queryFn: () => fetchUser(userId) }, { queryKey: ['posts', userId], queryFn: () => fetchPosts(userId) }, ], }); // 两个查询并行执行,都完成后组件渲染 return ; } ``` ### 乐观更新 (v5 简化) ```tsx // ❌ 手动管理缓存的乐观更新(复杂) const mutation = useMutation({ mutationFn: updateTodo, onMutate: async (newTodo) => { await queryClient.cancelQueries({ queryKey: ['todos'] }); const previousTodos = queryClient.getQueryData(['todos']); queryClient.setQueryData(['todos'], (old) => [...old, newTodo]); return { previousTodos }; }, onError: (err, newTodo, context) => { queryClient.setQueryData(['todos'], context.previousTodos); }, onSettled: () => { queryClient.invalidateQueries({ queryKey: ['todos'] }); }, }); // ✅ v5 简化:使用 variables 进行乐观 UI function TodoList() { const { data: todos } = useQuery(todosQueryOptions); const { mutate, variables, isPending } = useMutation({ mutationFn: addTodo, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['todos'] }); }, }); return (
    {todos?.map(todo => )} {/* 乐观显示正在添加的 todo */} {isPending && }
); } ``` ### v5 状态字段变化 ```tsx // v4: isLoading 表示首次加载或后续获取 // v5: isPending 表示没有数据,isLoading = isPending && isFetching const { data, isPending, isFetching, isLoading } = useQuery({...}); // isPending: 缓存中没有数据(首次加载) // isFetching: 正在请求中(包括后台刷新) // isLoading: isPending && isFetching(首次加载中) // ❌ v4 代码直接迁移 if (isLoading) return ; // v5 中行为可能不同 // ✅ 明确意图 if (isPending) return ; // 没有数据时显示加载 // 或 if (isLoading) return ; // 首次加载中 ``` --- ## Review Checklists ### Hooks 规则 - [ ] Hooks 在组件/自定义 Hook 顶层调用 - [ ] 没有条件/循环中调用 Hooks - [ ] useEffect 依赖数组完整 - [ ] useEffect 有清理函数(订阅/定时器/请求) - [ ] 没有用 useEffect 计算派生状态 ### 性能优化(适度原则) - [ ] useMemo/useCallback 只用于真正需要的场景 - [ ] React.memo 配合稳定的 props 引用 - [ ] 没有在组件内定义子组件 - [ ] 没有在 JSX 中创建新对象/函数(除非传给非 memo 组件) - [ ] 长列表使用虚拟化(react-window/react-virtual) ### 组件设计 - [ ] 组件职责单一,不超过 200 行 - [ ] 逻辑与展示分离(Custom Hooks) - [ ] Props 接口清晰,使用 TypeScript - [ ] 避免 Props Drilling(考虑 Context 或组合) ### 状态管理 - [ ] 状态就近原则(最小必要范围) - [ ] 复杂状态用 useReducer - [ ] 全局状态用 Context 或状态库 - [ ] 避免不必要的状态(派生 > 存储) ### 错误处理 - [ ] 关键区域有 Error Boundary - [ ] Suspense 配合 Error Boundary 使用 - [ ] 异步操作有错误处理 ### Server Components (RSC) - [ ] 'use client' 只用于需要交互的组件 - [ ] Server Component 不使用 Hooks/事件处理 - [ ] 客户端组件尽量放在叶子节点 - [ ] 数据获取在 Server Component 中进行 ### React 19 Forms - [ ] 使用 useActionState 替代多个 useState - [ ] useFormStatus 在 form 子组件中调用 - [ ] useOptimistic 不用于关键业务(支付等) - [ ] Server Action 正确标记 'use server' ### Suspense & Streaming - [ ] 按用户体验需求划分 Suspense 边界 - [ ] 每个 Suspense 有对应的 Error Boundary - [ ] 提供有意义的 fallback(骨架屏 > Spinner) - [ ] 避免在 layout 层级 await 慢数据 ### TanStack Query - [ ] queryKey 包含所有影响数据的参数 - [ ] 设置合理的 staleTime(不是默认 0) - [ ] useSuspenseQuery 不使用 enabled - [ ] Mutation 成功后 invalidate 相关查询 - [ ] 理解 isPending vs isLoading 区别 ### 测试 - [ ] 使用 @testing-library/react - [ ] 用 screen 查询元素 - [ ] 用 userEvent 代替 fireEvent - [ ] 优先使用 *ByRole 查询 - [ ] 测试行为而非实现细节