📄 本页由源文件
skills/dev-flow/references/react.md自动投影生成(单一权威源)。请勿直接编辑本页。
React 开发规范
基础底线(函数式组件、useEffect cleanup、memo/useMemo/useCallback、JSX 布尔属性、避免复杂表达式等)见
开发规范-红线.mdc§ React,本文不重复。本文为 step-5 按需加载的专项补充。
代码风格与结构
- 函数式 + 声明式编程,禁止 class 组件
- 组件定义使用
function关键字(非箭头函数),工具函数/回调仍可用箭头函数 - 偏向迭代和模块化,避免代码重复
- 描述性变量名:
isLoading、hasError、isSubmitting - 文件结构:导出组件 → 子组件 → 辅助函数 → 静态内容 → 类型
- 条件语句中简单语句省略不必要的大括号,使用简洁语法
命名约定
- 组件用 PascalCase:
UserProfile、ArticleCard - 偏向为组件使用命名导出(
export function UserProfile) - 目录用小写 + 短横线:
components/auth-wizard/ - 自定义 Hook 以
use开头:useDebounce、useIsMounted - 事件处理函数以
handle开头:handleClick、handleSubmit
TypeScript 使用
- 所有代码用 TypeScript,启用严格模式
- 优先用
interface定义 props 类型,而非type - 避免 enum,用字面量联合类型或
as const映射替代 - 组件 props 用接口定义:
interface UserCardProps { user: User; onEdit: (id: string) => void }
性能优化
- 避免不必要的重渲染:
React.memo包裹纯展示组件 - 稳定引用:非原始类型 props(对象/数组/回调)用
useMemo/useCallback - 惰性初始化:
useState(() => computeExpensiveValue()) - 图片优化:优先 WebP 格式,使用懒加载
- 代码分割:
React.lazy+Suspense分割非关键路由 - Portal:Modal、Tooltip、Dropdown 等需要脱离父容器 DOM 层级渲染时使用
createPortal
jsx
// ✅ 稳定的回调引用
const handleSave = useCallback((id: string) => {
saveData(id, formState);
}, [formState]);
// ❌ 每次渲染都产生新引用
const handleSave = (id: string) => saveData(id, formState);状态管理
- 简单局部状态用
useState+useReducer - 跨层级共享用 React Context,避免 props 层层传递
- 服务端数据用
react-query(@tanstack/react-query)管理缓存和请求 - 复杂全局状态考虑 Zustand(轻量)或 Redux Toolkit
- URL 状态(搜索参数、筛选条件)用路由参数,非本地 state
jsx
// react-query 典型用法
const { data, isLoading, error } = useQuery({
queryKey: ['article', articleId],
queryFn: () => fetchArticle(articleId),
enabled: !!articleId,
});错误处理
- 组件树顶层用 Error Boundary 兜底崩溃
- 事件处理中的错误必须捕获并提示用户,禁止空 catch
- 用早返回(early return)处理异常分支,避免深层嵌套
jsx
// ✅ 早返回
function ArticleDetail({ article }: { article?: Article }) {
if (!article) return <NotFound message='文章不存在' />;
if (article.status === 'archived') return <ArchivedNotice article={article} />;
return <ArticleContent article={article} />;
}
// ❌ 深层嵌套
function ArticleDetail({ article }: { article?: Article }) {
if (article) {
if (article.status !== 'archived') {
return <ArticleContent article={article} />;
} else {
return <ArchivedNotice article={article} />;
}
} else {
return <NotFound />;
}
}安全性
- 用户输入渲染前必须转义(React 默认防 XSS,但
dangerouslySetInnerHTML必须DOMPurify.sanitize) - 敏感数据(token/密钥)存放在环境变量,禁止硬编码
- API 通信强制 HTTPS,使用适当身份验证头
测试
- 单元测试:Jest + React Testing Library,侧重行为而非实现
- 关键用户流程:考虑集成测试覆盖(如 Cypress / Playwright)
- 纯工具函数必须有测试覆盖
常见错误模式
错误 1:props 内联对象导致无效重渲染
jsx
// ❌ 每次渲染 style 都是新对象,子组件必然重渲染
<Avatar style={{ width: 32, height: 32 }} />
// ✅ 静态值:提取到组件外
const AVATAR_SIZE = { width: 32, height: 32 };
<Avatar style={AVATAR_SIZE} />
// ✅ 依赖 props/state 的动态值:用 useMemo 稳定引用
const style = useMemo(() => ({ width: size, height: size }), [size]);
<Avatar style={style} />错误 2:条件渲染时 Hooks 顺序变化
jsx
// ❌ Hook 在条件分支内,违反 Hooks 规则
if (isAdmin) {
const data = useAdminData(); // bug: Hook 数量可能变化
}
// ✅ Hook 始终调用,内部做条件判断
const { data } = useQuery({
queryKey: ['admin', 'data'],
queryFn: fetchAdminData,
enabled: isAdmin,
});错误 3:状态提升过早
jsx
// ❌ 为单一组件提升到父级,增加传染范围
function Parent() {
const [open, setOpen] = useState(false);
return <Child open={open} onToggle={setOpen} />;
}
// ✅ 状态就近,需要共享时才提升
function Child() {
const [open, setOpen] = useState(false);
return <Modal open={open} onClose={() => setOpen(false)} />;
}useRef 模式
useRef 是除 useState/useEffect 外第三高频 Hook,两个核心用途:
用途 1:DOM 节点引用
jsx
function SearchInput() {
const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
// 组件挂载后自动聚焦
inputRef.current?.focus();
}, []);
return <input ref={inputRef} placeholder='搜索...' />;
}用途 2:可变值(不触发重渲染)
jsx
function Timer() {
const timerRef = useRef<ReturnType<typeof setInterval>>();
const [count, setCount] = useState(0);
useEffect(() => {
timerRef.current = setInterval(() => {
setCount(prev => prev + 1);
}, 1000);
return () => clearInterval(timerRef.current);
}, []);
return <span>{count}</span>;
}用途 3:保存前值
jsx
function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T>();
useEffect(() => { ref.current = value; });
return ref.current;
}⚠️ 关键区别:
ref.current = ...不触发重渲染,setState(...)触发。不要用 state 存储定时器 ID、动画实例等不需要驱动 UI 的值。
useLayoutEffect vs useEffect
useEffect:浏览器绘制后异步执行,适合数据请求、订阅useLayoutEffect:DOM 变更后、浏览器绘制前同步执行,适合 DOM 测量和同步更新
jsx
// ✅ 测量 DOM 尺寸后再渲染,避免闪烁
useLayoutEffect(() => {
setHeight(measureRef.current?.offsetHeight ?? 0);
}, [content]);受控 vs 非受控组件
React 表单元素的核心分歧点,混淆会导致 "A component is changing an uncontrolled input" warning。
- 受控:值由 React state 管理,需要
value+onChange(如<input value={name} onChange={handleChange} />) - 非受控:值由 DOM 自身管理,用
defaultValue+ref(如<input defaultValue='初始值' ref={ref} />)
jsx
// ❌ 从非受控切换到受控(或反之)触发 warning
<input value={isEditing ? text : undefined} /> // undefined → 非受控
// ✅ 始终受控,用空字符串代表空值
<input value={text ?? ''} onChange={handleChange} />
// ✅ 始终非受控,用 key 强制重新挂载来重置
<input key={resetKey} defaultValue='' />核心原则:给定 input 的生命周期内,
value属性 始终 为undefined(非受控)或始终为定义值(受控),禁止中途切换。
Keys 与列表渲染
jsx
// ❌ 用 index 做 key:列表重排序/增删时状态错乱
{todos.map((todo, index) => <TodoItem key={index} todo={todo} />)}
// ✅ 用稳定唯一标识做 key
{todos.map(todo => <TodoItem key={todo.id} todo={todo} />)}关键规则:
- key 必须在其兄弟节点中唯一且稳定
- 禁止
Math.random()/Date.now()作为 key(每次渲染都变,导致全部重挂载) - 可用
key强制重挂载组件(如重置表单、重新播放动画) - 无合适 ID 时用
crypto.randomUUID()在数据创建时生成,不要在渲染中生成
forwardRef + useImperativeHandle
封装组件时需要暴露内部 DOM 节点或命令式 API 给父组件时使用。
jsx
interface InputRef {
focus: () => void;
clear: () => void;
}
const CustomInput = forwardRef<InputRef, { label: string }>(({ label }, ref) => {
const inputRef = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => ({
focus: () => inputRef.current?.focus(),
clear: () => { if (inputRef.current) inputRef.current.value = ''; },
}));
return <label>{label}<input ref={inputRef} /></label>;
});
// 父组件使用
<CustomInput ref={inputRef} label='姓名' />⚠️ 优先通过 props 传递数据,仅在确实需要命令式控制(focus/scroll/measure)时才暴露 ref。
React Native / Expo 专项
适用场景:React Native 项目(含 Expo 托管 / 裸工作流)。
UI 与样式
- 使用 Flexbox +
useWindowDimensions实现响应式布局 - 深色模式:使用
useColorScheme适配 - 可访问性:使用 ARIA 角色和平台原生可访问属性
- 高性能动画与手势:使用
react-native-reanimated+react-native-gesture-handler
安全区域管理
- 全局用
SafeAreaProvider(来自react-native-safe-area-context) - 顶层组件使用
SafeAreaView包裹,处理刘海、状态栏等 - 可滚动内容用
SafeAreaScrollView - 禁止为安全区域硬编码 padding/margin,依赖 SafeAreaView 和 context hooks
导航
- 路由使用
react-navigation,遵循 Stack / Tab / Drawer 导航器最佳实践 - 使用深度链接(deep link)和通用链接(universal link)提升导航体验
- Expo 项目使用
expo-router的动态路由
国际化(i18n)
- 使用
react-native-i18n或expo-localization实现国际化 - 支持多语言和 RTL 布局
- 确保文本缩放和字体调整以保证可访问性
性能优化(RN 专属)
- 简单局部状态用
useState,复杂状态逻辑(多个子值/状态转换)用useReducer - 使用
AppLoading和SplashScreen优化启动体验 - 图片优化:优先 WebP,使用
expo-image实现延迟加载 - 使用
React.lazy+Suspense为非关键组件做代码拆分 - 通过
React.memo、useMemo、useCallback避免无效重渲染
错误处理补充
- 运行时校验使用 Zod 进行类型验证
- 使用 Sentry 或类似服务记录错误
- 生产环境使用
expo-error-reporter记录和上报错误 - Error Boundary:见主节「错误处理」
安全性补充
- 敏感数据使用
react-native-encrypted-storage安全存储 - 遵循 Expo 安全指南:Expo Security
测试(RN 专属)
- 单元测试:Jest + React Native Testing Library
- 关键用户流程集成测试:使用 Detox
- 使用 Expo 测试工具链在不同环境中运行测试
- 考虑为组件实施快照测试以保证 UI 一致性
Expo 关键约定
- 依赖 Expo 托管工作流简化开发和部署
- 优先考虑移动端核心指标(加载时间、卡顿、响应性)
- 使用
expo-constants管理环境变量和配置 - 使用
expo-permissions优雅处理设备权限 - 实施
expo-updates进行 OTA 更新 - 遵循 Expo 部署和发布最佳实践:Expo Distribution
- 在 iOS 和 Android 双平台上广泛测试确保兼容性