React 最佳实践

FreeGuideOnline 最新 2026-07-14

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 useMemouseCallback

  • useMemo:缓存计算结果,避免每次渲染都重复复杂运算
  • useCallback:缓存函数引用,避免子组件因函数引用变化而无意义重渲染
const sortedList = useMemo(() => list.sort(compareFn), [list]);
const handleClick = useCallback(() => {
  doSomething(id);
}, [id]);

注意:不要滥用,仅在性能瓶颈出现时优化。

5.3 懒加载与代码分割

使用 React.lazySuspense 实现路由级或组件级拆分。

const LazyDashboard = React.lazy(() => import('./Dashboard'));

<Suspense fallback={<Spinner />}>
  <LazyDashboard />
</Suspense>

5.4 虚拟化长列表

对于渲染大量数据的列表,使用 react-windowreact-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 条件类名

使用 clsxclassnames 库动态拼接类名。

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 />;
}