React 最佳实践
src/ ├── features/ │ ├── auth/ │ │ ├── components/ │ │ ├── hooks/ │ │ ├── services/ │ │ └── types/ │ ├── dashboard/ │ └── ... ├── shared/ │ ├── components/ │ ├── hooks/ │ └── utils/ └── ...
**优点**:相关代码聚合在一起,方便定位和修改;团队协作时可减少文件冲突。
### 1.2 组件文件命名
- 组件文件名与组件名完全一致,使用 **PascalCase**:`UserProfile.tsx`
- 每个文件只导出一个组件,除非是非常紧密的纯逻辑组件(如 `List.Item`)
### 1.3 公共代码抽象到 `shared` 层
将跨模块使用的 UI 组件、自定义 Hook、工具函数放入 `shared` 目录,但必须保证它们无业务逻辑、通用且经过充分测试。
---
## 2. 组件设计
### 2.1 组件单一职责
每个组件只做一件事,并且结构尽量扁平。
❌ 一个组件同时负责展示用户信息、处理表单验证、调用 API
✅ 拆分为 `<UserInfo>`、`<UserEditForm>` 和自定义 Hook `useUserData`
### 2.2 展示组件与容器组件分离
- **展示组件**:无状态,只负责 UI 渲染,通过 props 接收数据和回调。
- **容器组件**:管理状态、订阅数据、处理业务逻辑。
```tsx
// 展示组件
const UserCard: React.FC<{ name: string; onEdit: () => void }> = ({ name, onEdit }) => (
<div>
<span>{name}</span>
<button onClick={onEdit}>Edit</button>
</div>
);
// 容器 Hook
function useUser() {
const [user, setUser] = useState(null);
useEffect(() => {
fetchUser().then(setUser);
}, []);
return { user, editUser };
}
2.3 合理使用组合而非继承
React 推崇组合模式。使用 children 或特定插槽(slots)来让组件更灵活。
function Card({ header, children }) {
return (
<div className="card">
<div className="card-header">{header}</div>
<div className="card-body">{children}</div>
</div>
);
}
3. 状态管理
3.1 优先使用局部状态
不要过早将状态提升到全局。判断标准:
- 只有一个组件关心的状态 →
useState - 父子或兄弟组件需要共享 → 提升到最近的共同父级
- 多个不相干组件需要 → 才考虑全局状态管理(Context、Redux、Zustand 等)
3.2 谨慎使用 Context
React Context 非常适合全局主题、用户认证信息等,但不适合频繁更新的状态,因为它的更新会导致所有消费者重新渲染。
const ThemeContext = React.createContext('light');
常见替代方案:对于高频率变化的状态,推荐使用 Zustand 或 Redux Toolkit,它们内置了选择器机制来避免不必要的渲染。
3.3 不可变更新数据
永远不要直接修改 state,而是创建新的引用。
// 错误
state.items.push(newItem);
setState(state);
// 正确
setState(prev => ({
...prev,
items: [...prev.items, newItem]
}));
推荐:复杂状态更新使用 Immer.js 简化不可变操作。
4. Hooks 使用规范
4.1 自定义 Hook 抽象逻辑
将可复用的有状态逻辑封装成自定义 Hook,命名以 use 开头。
function useOnlineStatus() {
const [online, setOnline] = useState(navigator.onLine);
useEffect(() => {
const handle = () => setOnline(navigator.onLine);
window.addEventListener('online', handle);
window.addEventListener('offline', handle);
return () => {
window.removeEventListener('online', handle);
window.removeEventListener('offline', handle);
};
}, []);
return online;
}
4.2 遵守 Hook 规则
- 只在组件顶层调用 Hook,不要在循环、条件或嵌套函数中调用
- 只在 React 函数组件或自定义 Hook 中调用 Hook
4.3 合理使用 useEffect
useEffect 是用来同步外部系统的,而不是编排组件生命周期。确保正确设置依赖项数组。
useEffect(() => {
const subscription = api.subscribe(userId);
return () => subscription.unsubscribe();
}, [userId]); // 必须包含所有在 effect 中使用的响应值
- 避免将对象、数组作为依赖(除非使用
useMemo保持引用稳定) - 尽量减少
useEffect的数量,多个无关副作用可分开
5. 性能优化
5.1 使用 React.memo 避免无用渲染
对于纯展示组件,用 React.memo 包裹,仅在 props 变化时重新渲染。
const UserCard = React.memo(({ name }) => <div>{name}</div>);
5.2 useMemo 与 useCallback
useMemo:缓存计算结果,避免每次渲染都重复复杂运算useCallback:缓存函数引用,避免子组件因函数引用变化而无意义重渲染
const sortedList = useMemo(() => list.sort(compareFn), [list]);
const handleClick = useCallback(() => {
doSomething(id);
}, [id]);
注意:不要滥用,仅在性能瓶颈出现时优化。
5.3 懒加载与代码分割
使用 React.lazy 和 Suspense 实现路由级或组件级拆分。
const LazyDashboard = React.lazy(() => import('./Dashboard'));
<Suspense fallback={<Spinner />}>
<LazyDashboard />
</Suspense>
5.4 虚拟化长列表
对于渲染大量数据的列表,使用 react-window 或 react-virtuoso 只渲染视口内的元素。
6. 代码质量与可维护性
6.1 使用 TypeScript
为所有组件添加严格的类型定义,包括 props、state 和自定义 Hook 的返回值。
interface ButtonProps {
variant: 'primary' | 'secondary';
size?: 'sm' | 'md' | 'lg';
onClick: () => void;
children: React.ReactNode;
}
6.2 代码规范工具
- ESLint 搭配
eslint-plugin-react-hooks检查 Hook 规则 - Prettier 统一代码格式
- Husky + lint-staged 在提交前自动修复
6.3 编写可测试的组件
- 保持组件纯粹,将副作用与 UI 剥离
- 逻辑通过自定义 Hook 暴露,便于单独测试
- 使用
@testing-library/react从用户角度编写测试
7. 样式管理
7.1 选择适合的 CSS 方案
- CSS Modules:作用域隔离,适合传统项目
- Tailwind CSS:原子化类名,快速构建 UI
- Styled Components / Emotion:动态样式,组件级样式
保持一致,避免多种方案混用。
7.2 条件类名
使用 clsx 或 classnames 库动态拼接类名。
import clsx from 'clsx';
<div className={clsx('base', { active: isActive, disabled: !enabled })} />
8. 常见反模式与陷阱
8.1 避免在渲染中创建组件
// 错误:每次渲染都会创建新的组件定义
function Parent() {
const Child = () => <div>child</div>;
return <Child />;
}
8.2 不将索引作为 key
使用唯一且稳定的标识符作为列表项的 key,不要使用数组索引,否则在列表顺序改变或增删时会导致状态错乱。
items.map(item => <Item key={item.id} item={item} />)
8.3 避免过度嵌套的三元表达式
提取清晰的条件渲染函数或使用 if/else。
function Status({ code }) {
if (code === 'loading') return <Spinner />;
if (code === 'error') return <Error />;
return <Data />;
}