React 模式与 Hooks
指导 Agent 使用自定义 Hook 抽取、状态提升与数据获取模式,并遵守 eslint-plugin-react-hooks 与项目目录约定。
SKILL 应区分服务端组件/客户端组件边界(若使用 App Router)、useEffect 的合法用途与替代方案(事件、key、派生状态),以及何时用 useMemo/useCallback 而非默认包裹。
数据获取写明与 TanStack Query、SWR 或 RSC fetch 的选用规则;错误与加载态在 Suspense 边界下的表现要可预测。
Context 使用需注明粒度与拆分策略,避免全局大对象导致无效重渲染;列表 key 与可访问性属性在模式说明中一并要求。
- 严格模式与并发特性:若开启,说明对 effect 双调用的预期与测试写法。
- 表单:受控与非受控、与 zod 等校验库的集成入口。
- 测试:@testing-library 查询优先级与「不测实现细节」的断言习惯。
Custom Hook 与 Context+Reducer 状态管理
// Custom Hook 设计原则:useAsync — 封装异步请求状态
function useAsync<T>(asyncFn: () => Promise<T>, deps: React.DependencyList) {
const [state, dispatch] = React.useReducer(asyncReducer<T>, {
status: 'idle', data: null, error: null
});
React.useEffect(() => {
let cancelled = false; // 防止竞态条件
dispatch({ type: 'LOADING' });
asyncFn().then(
data => { if (!cancelled) dispatch({ type: 'SUCCESS', data }); },
error => { if (!cancelled) dispatch({ type: 'ERROR', error }); }
);
return () => { cancelled = true; }; // 清理:组件卸载或 deps 变化时取消
}, deps); // eslint: exhaustive-deps
return state;
}
// useDebounce:防抖,避免频繁触发 API
function useDebounce<T>(value: T, delayMs: number): T {
const [debounced, setDebounced] = React.useState(value);
React.useEffect(() => {
const timer = setTimeout(() => setDebounced(value), delayMs);
return () => clearTimeout(timer); // 清理上一个 timer
}, [value, delayMs]);
return debounced;
}
// Context + Reducer:代替 Redux 的简单全局状态方案
type CartAction =
| { type: 'ADD_ITEM'; item: CartItem }
| { type: 'REMOVE_ITEM'; id: string }
| { type: 'CLEAR' };
function cartReducer(state: CartState, action: CartAction): CartState {
switch (action.type) {
case 'ADD_ITEM':
return { ...state, items: [...state.items, action.item] };
case 'REMOVE_ITEM':
return { ...state, items: state.items.filter(i => i.id !== action.id) };
case 'CLEAR':
return { items: [] };
}
}
// Provider:dispatch 和 state 分开 Context,避免无效重渲染
const CartStateCtx = React.createContext<CartState | null>(null);
const CartDispatchCtx = React.createContext<React.Dispatch<CartAction> | null>(null);
export function CartProvider({ children }) {
const [state, dispatch] = React.useReducer(cartReducer, { items: [] });
return (
<CartStateCtx.Provider value={state}>
<CartDispatchCtx.Provider value={dispatch}>
{children}
</CartDispatchCtx.Provider>
</CartStateCtx.Provider>
);
}
- 单一职责:一个文件默认一个主导出组件;复杂 UI 用子组件或同目录私有模块拆分,避免「千行 JSX」。
- 复合组件(Compound):父级通过 Context 或显式子组件 API(如
Tabs.Tab)共享隐式状态,对外保持稳定契约。 - 受控封装:表单控件在库组件边界统一 value/onChange 形状,避免半受控混用导致状态撕裂。
组合与插槽
优先用 children、render prop 或显式插槽 props 组合 UI,而不是在父组件里用一长串条件渲染子类型字符串。SKILL 可要求「扩展点」一律通过组合暴露,便于类型推断与测试替换。
┌──────────────────┐
│ Parent │
│ state / 数据 │
└────────┬─────────┘
│ props.children / render prop / 具名插槽
┌────────┴────────┐
▼ ▼
┌────────────┐ ┌────────────┐
│ Region A │ │ Region B │
│ (展示/局部) │ │ (列表/表单) │
└────────────┘ └────────────┘
│
可选:共享隐式状态 via Context(粒度要小)
Render prop 适合「把同一套逻辑交给不同 UI」;children 作为函数时记得在类型里标清参数,避免 Agent 生成隐式 any。
性能优化、Error Boundary 与 Suspense
// useMemo/useCallback 正确使用场景(默认不加,有原因才加)
// ✅ 正确:子组件已 memo,计算昂贵,引用稳定才有意义
const sortedItems = React.useMemo(
() => items.sort((a, b) => a.price - b.price),
[items] // 只在 items 变化时重算,注释:排序 O(n log n) 避免每次渲染重算
);
const handleSubmit = React.useCallback(
(data: FormData) => onSubmit(userId, data),
[userId, onSubmit] // 注释:作为 memo 子组件的 prop,稳定引用避免重渲染
);
// ❌ 错误:包裹简单计算或原始值(无意义的优化)
// const title = useMemo(() => `Hello ${name}`, [name]); — 直接写即可
// Error Boundary 实现(类组件,只有 class 支持 componentDidCatch)
class ErrorBoundary extends React.Component<
{ fallback: React.ReactNode; children: React.ReactNode },
{ error: Error | null }
> {
state = { error: null };
static getDerivedStateFromError(error: Error) {
return { error }; // 触发降级 UI
}
componentDidCatch(error: Error, info: React.ErrorInfo) {
console.error('ErrorBoundary caught:', error, info.componentStack);
reportError(error); // 上报到监控(Sentry 等)
}
render() {
if (this.state.error) return this.props.fallback;
return this.props.children;
}
}
// Suspense + lazy 代码分割
const HeavyEditor = React.lazy(() => import('./HeavyEditor'));
const ChartDashboard = React.lazy(() => import('./ChartDashboard'));
function App() {
return (
<ErrorBoundary fallback={<div>组件加载失败,请刷新页面</div>}>
<React.Suspense fallback={<Skeleton />}> {/* 降级 UI */}
<HeavyEditor />
</React.Suspense>
</ErrorBoundary>
);
}
// 路由级分割(React Router)
const routes = [
{ path: '/dashboard', element: <React.Suspense fallback={<PageSkeleton />}><ChartDashboard /></React.Suspense> },
];
- 依赖数组:对
useEffect/useMemo/useCallback使用 exhaustive-deps;遗漏依赖导致陈旧闭包,过度填入内联对象导致多余执行。 - Effect 用途:同步外部系统(订阅、DOM、非 React 存储);能用事件处理、派生计算或
key重置解决的,不要塞进 effect。 - 清理函数:订阅与计时器必须在 cleanup 中释放;严格模式下开发环境可能对 effect 进行双挂载以暴露缺失清理的问题。
setState(s => …) 或 useRef 保存最新句柄(按场景选用)。
页内工具:依赖说明与 Hooks 规则自测
以下脚本仅在本页运行,用于快速理解 useEffect 依赖语义并巩固 Hooks 规则(非 linter 替代品)。
useEffect 依赖数组说明生成
输入逗号分隔的依赖项(与源码中依赖数组一致),例如:userId, query, options。单独输入 [] 表示空数组。
Hooks 规则小测(4 题)
每题选一;提交后显示得分与简要提示。
SKILL 大纲
---
name: react-patterns-hooks
description: 按 Hooks 规则、状态管理与性能模式实现 React 功能
---
# Custom Hook 设计
1. use 前缀命名,封装「有状态逻辑」而非 UI
2. useAsync:管理 loading/data/error 三态,cleanup 防竞态
3. useDebounce:返回防抖值,cleanup 清理 timer
4. Context 拆分:state 和 dispatch 分开 Provider,避免无效重渲染
# Context + Reducer
5. useReducer 管理复杂状态,reducer 为纯函数、可单独测试
6. dispatch 函数引用稳定,无需加入 useCallback 依赖
7. 拆分 Context:频繁变化的状态用独立 Context
# 性能优化
8. useMemo:昂贵计算 + 已 memo 子组件使用;写注释说明原因
9. useCallback:作为 memo 子组件 prop 或其他 Hook 依赖时加;否则不加
10. 默认不优化,Profile 发现问题后才加,并注释理由
# Error Boundary 与 Suspense
11. ErrorBoundary 包裹可能出错的子树,fallback 给用户提示而非白屏
12. componentDidCatch 上报错误到监控系统(Sentry/自建)
13. React.lazy + Suspense 分割大依赖(编辑器/图表/PDF 预览等)
14. Suspense fallback 用 Skeleton 而非 null(避免 CLS)
# Hooks 规则
15. 只在函数组件/自定义 Hook 顶层调用;不在循环/条件/嵌套函数里调用