19-ReactJS最佳实践

19、ReactJS - 最佳实践(Best Practices):从入门到企业级落地的完整指南

写了三年 React,代码还是一团糟?改一个 bug 引出三个 bug?页面输入一个字卡三秒?这篇文章将系统梳理 React 最佳实践,从是什么、为什么、怎么演进、怎么用到企业级落地,配合真实代码和面试高频题,让你看完就能直接上手。


文章目录

一、痛点场景:那些年我们一起踩过的 React 坑

场景一:状态满天飞,改一个 bug 引出三个 bug

你有没有遇到过这样的场景:项目里有一个用户信息,在 A 组件里存了一份,在 B 组件里又存了一份,在 C 组件的 props 里还有一份。某天产品说要改个用户昵称,你改了 A 组件的状态,结果 B 组件显示的还是旧数据,C 组件直接报错了。

更可怕的是,你不知道这个状态到底有多少个地方在改,每次修改都像在拆炸弹,生怕哪里又炸了。

场景二:上帝组件,一个文件 2000 行

打开项目,找到那个叫 UserPage.jsx 的文件,往下滚,往下滚,再往下滚......2000 行。这个组件里有数据请求、表单验证、权限判断、UI 渲染、业务逻辑,甚至还有一段计算价格的复杂算法。

新人入职想看懂这个组件,得花三天。想加个功能?先祈祷别碰坏别的逻辑。

场景三:页面卡顿,输入一个字卡三秒

搜索框里输入一个字,整个页面都在抖。打开 React DevTools 一看,父组件一渲染,下面几十个子组件全跟着重新渲染。明明只是改了个输入框的值,为什么整个列表都要重新算一遍?

场景四:Hooks 依赖数组地狱

javascript 复制代码
useEffect(() => {
  fetchData(userId, page, filter, sortBy, keyword);
}, [userId, page, filter, sortBy, keyword]);

依赖数组越写越长,少写一个就出 bug,多写一个就无限循环。每次改这个 effect,都得仔细检查一遍依赖,生怕漏了什么。


二、痛点的解决方案:React 最佳实践全景图

上面这些问题,本质上都是没有遵循 React 最佳实践导致的。React 最佳实践是社区在多年开发中总结出来的一套规范和模式,涵盖状态管理、组件设计、性能优化、Hooks 规范、工程化等多个维度。

遵循最佳实践,你将获得:

  • 可维护的代码:改一个地方不会牵一发而动全身
  • 高性能的应用:该渲染的渲染,不该渲染的绝不渲染
  • 可协作的团队:新人来了能快速上手,代码风格统一
  • 可扩展的架构:业务增长时不需要推倒重来

三、是什么:React 最佳实践的专业定义

专业解释

React 最佳实践(Best Practices)是指在 React 应用开发过程中,经过社区大量项目验证、能够显著提升代码质量、可维护性、性能和开发效率的一系列编码规范、设计模式和架构原则的集合。

它主要包含五大核心领域:

  1. 状态管理:合理组织和管理应用状态,避免冗余和矛盾
  2. 组件设计:遵循单一职责原则,通过组合而非继承构建 UI
  3. Hooks 规范:正确使用 Hooks,遵守调用规则和依赖管理
  4. 性能优化:减少不必要的渲染,提升应用响应速度
  5. 工程化:目录结构、代码规范、类型检查、测试等配套设施

大白话

说白了,React 最佳实践就是"老司机们踩了无数坑之后总结出来的正确开车姿势"。

你可以把它想象成做菜的菜谱:同样的食材,按照菜谱来做,味道就不会差;自己瞎搞,很可能做成黑暗料理。React 最佳实践就是那本菜谱,告诉你状态该怎么放、组件该怎么拆、性能该怎么调。

生活案例

就像装修房子:

  • 状态管理 = 东西分类放好,厨房的放厨房,卧室的放卧室,不要到处乱扔
  • 组件设计 = 每个房间有明确的功能,卧室就是睡觉的,厨房就是做饭的,不要在卧室里炒菜
  • Hooks 规范 = 电器使用说明书,告诉你这个开关不能乱按,按错了会跳闸
  • 性能优化 = 合理安排动线,拿个菜不用绕整个房子一圈
  • 工程化 = 装修图纸和施工标准,保证每个工人都按同一套规范来

不按最佳实践来,就像房子里东西乱扔、卧室里炒菜、电线乱拉------短期能住,长期一定出问题。


四、为什么用:不遵守最佳实践会怎样

技术债务越积越多

不遵循最佳实践的项目,就像不按时还款的信用卡,利息越滚越大。一开始图快写的代码,后面要花十倍的时间去维护。

团队协作效率低下

每个人都有自己的写法,A 同学喜欢把逻辑放 render 里,B 同学喜欢用 Class 组件,C 同学写的 Hooks 依赖数组永远缺一个。代码 review 变成了风格吵架,合并代码天天冲突。

性能问题频发

不必要的重渲染、巨大的组件、混乱的状态更新,最终都会反映到用户体验上:页面卡顿、加载缓慢、交互延迟。用户可不会管你代码写得有多"巧妙",卡了就是卡了。

Bug 难以定位和修复

状态散落在各处,组件职责不清,出了 bug 不知道从哪查起。好不容易找到原因,改了又怕影响别的地方,陷入"修一个 bug 造三个 bug"的死循环。


五、演进历程:React 最佳实践是怎么一步步走到今天的

第一阶段:Class 组件时代(2013-2018)

早期的 React 用 Class 组件,状态通过 this.state 管理,生命周期方法(componentDidMount、componentDidUpdate、componentWillUnmount)处理副作用。

javascript 复制代码
class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }

  componentDidMount() {
    document.title = `Count: ${this.state.count}`;
  }

  componentDidUpdate() {
    document.title = `Count: ${this.state.count}`;
  }

  render() {
    return (
      <button onClick={() => this.setState({ count: this.state.count + 1 })}>
        Clicked {this.state.count} times
      </button>
    );
  }
}

问题 :逻辑分散在多个生命周期方法中,this 绑定让人头疼,逻辑复用困难。

第二阶段:Mixins 时代(已废弃)

为了解决逻辑复用问题,React 早期提供了 Mixins 机制,可以把公共逻辑混入多个组件。

javascript 复制代码
const LoggingMixin = {
  componentDidMount() {
    console.log('Component mounted');
  }
};

const MyComponent = React.createClass({
  mixins: [LoggingMixin],
  render() {
    return <div>Hello</div>;
  }
});

问题:Mixins 会导致命名冲突、隐式依赖、复杂度增加,后来被官方废弃。

第三阶段:HOC 高阶组件时代(2016-2019)

Mixins 被废弃后,社区转向高阶组件(Higher-Order Components)模式:一个函数接收组件,返回增强后的新组件。

javascript 复制代码
function withLogging(WrappedComponent) {
  return class extends React.Component {
    componentDidMount() {
      console.log('Component mounted');
    }
    render() {
      return <WrappedComponent {...this.props} />;
    }
  };
}

const EnhancedComponent = withLogging(MyComponent);

问题 :嵌套地狱(withAuth(withRouter(withLogging(MyComponent))))、props 来源不清晰、ref 传递麻烦。

第四阶段:Hooks 时代(2019 至今)

React 16.8 引入 Hooks,彻底改变了 React 的开发方式。函数组件也能拥有状态和副作用,逻辑复用变得简单优雅。

javascript 复制代码
function Counter() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `Count: ${count}`;
  }, [count]);

  return (
    <button onClick={() => setCount(count + 1)}>
      Clicked {count} times
    </button>
  );
}

优势 :逻辑聚合、复用简单、代码更短、没有 this 烦恼。

第五阶段:React Compiler 时代(2025 开启)

2025 年 10 月,React 团队发布了 React Compiler v1.0。它能在构建时自动进行记忆化优化,让开发者不再需要手动写 useMemo、useCallback、React.memo。

这意味着性能优化的最佳实践正在从"手动优化"转向"编译器自动优化",开发者可以更专注于业务逻辑本身。


六、怎么用:五大核心最佳实践实战

6.1 状态管理最佳实践

原则一:状态就近放置,能不下放就不下放

状态应该放在离使用它最近的组件里。只有当多个组件需要共享同一份状态时,才把状态提升到它们的最近公共祖先。

反面教材:把所有状态都放到顶层 App 组件,导致任何一个小状态变化都会触发整个应用重渲染。

正确做法:

javascript 复制代码
// 错误:把搜索关键词放到全局
function App() {
  const [keyword, setKeyword] = useState('');
  return (
    <div>
      <Header />
      <SearchPage keyword={keyword} setKeyword={setKeyword} />
      <Footer />
    </div>
  );
}

// 正确:搜索关键词只在 SearchPage 内部使用
function SearchPage() {
  const [keyword, setKeyword] = useState('');
  return (
    <div>
      <input value={keyword} onChange={(e) => setKeyword(e.target.value)} />
      <SearchResults keyword={keyword} />
    </div>
  );
}
原则二:避免冗余状态,能计算就不存储

如果一个值可以通过 props 或其他状态计算出来,就不要把它单独存为状态。

反面教材 :同时存 firstName、lastName 和 fullName,改了 firstName 忘了改 fullName。

正确做法:

javascript 复制代码
function UserProfile({ user }) {
  // 错误:冗余状态
  // const [fullName, setFullName] = useState(`${user.firstName} ${user.lastName}`);

  // 正确:渲染时计算
  const fullName = `${user.firstName} ${user.lastName}`;

  return <div>{fullName}</div>;
}
原则三:相关状态分组,避免状态矛盾

如果多个状态总是一起更新,考虑把它们合并成一个对象或使用 useReducer。

javascript 复制代码
// 分散的状态,容易出现矛盾
const [isLoading, setIsLoading] = useState(false);
const [data, setData] = useState(null);
const [error, setError] = useState(null);

// 合并后,状态更一致
const [state, setState] = useState({
  status: 'idle', // idle | loading | success | error
  data: null,
  error: null
});

// 或者使用 useReducer 管理复杂状态
function dataReducer(state, action) {
  switch (action.type) {
    case 'FETCH_START':
      return { status: 'loading', data: null, error: null };
    case 'FETCH_SUCCESS':
      return { status: 'success', data: action.payload, error: null };
    case 'FETCH_ERROR':
      return { status: 'error', data: null, error: action.payload };
    default:
      return state;
  }
}
原则四:不可变更新,永远不要直接修改状态

React 依赖状态的引用来判断是否需要重新渲染。直接修改状态对象,React 检测不到变化,会导致各种诡异 bug。

javascript 复制代码
const [user, setUser] = useState({ name: '张三', age: 25 });

// 错误:直接修改
user.age = 26;
setUser(user);

// 正确:创建新对象
setUser({ ...user, age: 26 });

// 数组更新同理
const [list, setList] = useState([1, 2, 3]);

// 错误
list.push(4);
setList(list);

// 正确
setList([...list, 4]);
setList(list.filter(item => item !== 2));
setList(list.map(item => item * 2));

6.2 组件设计最佳实践

原则一:单一职责,一个组件只做一件事

如果一个组件承担了太多职责,就把它拆成多个小组件。判断标准:如果你在描述组件功能时用了"和"、"并且"、"还要",那它可能就该拆了。

反面教材 :一个 UserPage 组件同时负责数据获取、表单验证、列表渲染、弹窗管理。

正确拆分:

复制代码
UserPage/
├── index.jsx          // 容器组件:数据获取和状态管理
├── UserList.jsx       // 展示组件:用户列表渲染
├── UserForm.jsx       // 表单组件:新增/编辑用户
├── UserDetail.jsx     // 详情组件:用户详情展示
└── useUsers.js        // 自定义 Hook:用户相关逻辑
javascript 复制代码
// 容器组件:负责数据和逻辑
function UserPage() {
  const { users, loading, error, addUser, deleteUser } = useUsers();

  if (loading) return <Loading />;
  if (error) return <Error message={error.message} />;

  return (
    <div>
      <UserForm onSubmit={addUser} />
      <UserList users={users} onDelete={deleteUser} />
    </div>
  );
}

// 展示组件:只负责渲染
function UserList({ users, onDelete }) {
  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>
          {user.name}
          <button onClick={() => onDelete(user.id)}>删除</button>
        </li>
      ))}
    </ul>
  );
}
原则二:组合优于继承

React 不推荐用继承来复用组件逻辑,而是通过组合(props 传递、children 嵌套、自定义 Hook)来实现。

javascript 复制代码
// 用组合实现布局复用
function Layout({ header, sidebar, children }) {
  return (
    <div className="layout">
      <header>{header}</header>
      <div className="main">
        <aside>{sidebar}</aside>
        <content>{children}</content>
      </div>
    </div>
  );
}

// 使用时组合各个部分
function DashboardPage() {
  return (
    <Layout
      header={<Header />}
      sidebar={<Sidebar />}
    >
      <DashboardContent />
    </Layout>
  );
}
原则三:Props 类型校验,提前发现问题

始终定义组件的 Props 类型,无论是用 TypeScript 还是 PropTypes。这能在开发阶段就发现类型错误,而不是等到运行时才崩溃。

typescript 复制代码
// TypeScript 方式
interface UserCardProps {
  user: {
    id: number;
    name: string;
    avatar: string;
  };
  size?: 'small' | 'medium' | 'large';
  onClick?: (id: number) => void;
}

function UserCard({ user, size = 'medium', onClick }: UserCardProps) {
  return (
    <div className={`user-card user-card--${size}`} onClick={() => onClick?.(user.id)}>
      <img src={user.avatar} alt={user.name} />
      <span>{user.name}</span>
    </div>
  );
}

6.3 Hooks 使用规范

规则一:只在顶层调用 Hooks

不要在循环、条件判断或嵌套函数中调用 Hooks。Hooks 的调用顺序必须在每次渲染中保持一致,React 靠这个顺序来对应状态。

javascript 复制代码
function UserProfile({ userId }) {
  // 错误:在条件中调用 Hook
  if (userId) {
    const [user, setUser] = useState(null);
  }

  // 正确:把条件放到 Hook 内部
  const [user, setUser] = useState(null);

  useEffect(() => {
    if (!userId) return;
    fetchUser(userId).then(setUser);
  }, [userId]);
}
规则二:只在 React 函数中调用 Hooks

Hooks 只能在函数组件或自定义 Hook 中调用,不能在普通 JavaScript 函数中调用。

规则三:依赖数组要完整,不要欺骗 React

useEffect、useMemo、useCallback 的依赖数组必须包含所有在回调中使用的响应式值(props、state 以及由它们派生的值)。

javascript 复制代码
function SearchResults({ keyword, filter }) {
  const [results, setResults] = useState([]);

  // 错误:漏掉了 filter 依赖
  useEffect(() => {
    fetchResults(keyword, filter).then(setResults);
  }, [keyword]);

  // 正确:完整的依赖数组
  useEffect(() => {
    fetchResults(keyword, filter).then(setResults);
  }, [keyword, filter]);
}

建议安装 eslint-plugin-react-hooks,它会自动检查依赖数组的完整性。

规则四:自定义 Hook 抽取可复用逻辑

把多个组件共用的逻辑抽取成自定义 Hook,这是 React 中最优雅的逻辑复用方式。

javascript 复制代码
// 自定义 Hook:数据请求
function useFetch(url, options = {}) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState(null);

  useEffect(() => {
    let cancelled = false;

    async function fetchData() {
      setLoading(true);
      setError(null);
      try {
        const response = await fetch(url, options);
        const result = await response.json();
        if (!cancelled) setData(result);
      } catch (err) {
        if (!cancelled) setError(err);
      } finally {
        if (!cancelled) setLoading(false);
      }
    }

    fetchData();

    return () => {
      cancelled = true;
    };
  }, [url]);

  return { data, loading, error };
}

// 在组件中使用
function UserList() {
  const { data: users, loading, error } = useFetch('/api/users');

  if (loading) return <Loading />;
  if (error) return <Error message={error.message} />;

  return <ul>{users.map(user => <li key={user.id}>{user.name}</li>)}</ul>;
}

6.4 性能优化最佳实践

原则一:先测量再优化,不要过早优化

不要一上来就给所有组件包 React.memo,给所有计算包 useMemo。先使用 React DevTools 的 Profiler 工具找到真正的性能瓶颈,再有针对性地优化。

记忆化本身也有成本,滥用反而会让性能更差。

原则二:合理使用 React.memo

React.memo 可以让组件在 props 没有变化时跳过重新渲染。但它只对浅比较有效,如果 props 是对象或函数,每次渲染都会创建新引用,React.memo 就失效了。

javascript 复制代码
// 适合用 memo 的纯展示组件
const UserCard = React.memo(function UserCard({ user, onClick }) {
  return (
    <div onClick={() => onClick(user.id)}>
      {user.name}
    </div>
  );
});
原则三:useMemo 缓存昂贵计算

只有当计算确实昂贵(比如遍历大数组、复杂排序、数据转换)时,才用 useMemo。简单的计算不需要缓存。

javascript 复制代码
function ProductList({ products, category }) {
  // 昂贵计算:过滤+排序+格式化,适合用 useMemo
  const visibleProducts = useMemo(() => {
    return products
      .filter(p => p.category === category)
      .sort((a, b) => a.price - b.price)
      .map(p => ({
        ...p,
        formattedPrice: `¥${p.price.toFixed(2)}`
      }));
  }, [products, category]);

  return (
    <ul>
      {visibleProducts.map(p => (
        <li key={p.id}>{p.name} - {p.formattedPrice}</li>
      ))}
    </ul>
  );
}
原则四:useCallback 稳定函数引用

当你需要把函数作为 props 传给用了 React.memo 的子组件时,用 useCallback 保持函数引用稳定。

javascript 复制代码
function Parent() {
  const [count, setCount] = useState(0);

  // 用 useCallback 保持函数引用稳定
  const handleIncrement = useCallback(() => {
    setCount(c => c + 1);
  }, []);

  return (
    <div>
      <span>{count}</span>
      <MemoizedChild onIncrement={handleIncrement} />
    </div>
  );
}
原则五:拆分 Context,避免全局重渲染

Context 的值变化时,所有消费这个 Context 的组件都会重新渲染。不要把所有状态都塞到一个大 Context 里,应该按领域拆分。

javascript 复制代码
// 错误:一个巨大的 AppContext
const AppContext = createContext();

function AppProvider({ children }) {
  const [user, setUser] = useState(null);
  const [theme, setTheme] = useState('light');
  const [notifications, setNotifications] = useState([]);
  // 用户信息变化会导致主题组件也重渲染
  return (
    <AppContext.Provider value={{ user, theme, notifications }}>
      {children}
    </AppContext.Provider>
  );
}

// 正确:按领域拆分
const UserContext = createContext();
const ThemeContext = createContext();
const NotificationContext = createContext();

function AppProvider({ children }) {
  return (
    <UserProvider>
      <ThemeProvider>
        <NotificationProvider>
          {children}
        </NotificationProvider>
      </ThemeProvider>
    </UserProvider>
  );
}
原则六:长列表虚拟化

如果需要渲染成百上千条列表数据,使用虚拟滚动(react-window 或 react-virtualized),只渲染可视区域内的元素。

javascript 复制代码
import { FixedSizeList as List } from 'react-window';

function LongList({ items }) {
  const Row = ({ index, style }) => (
    <div style={style}>
      {items[index].name}
    </div>
  );

  return (
    <List
      height={500}
      itemCount={items.length}
      itemSize={50}
      width="100%"
    >
      {Row}
    </List>
  );
}

6.5 工程化最佳实践

目录结构:按功能组织,不要按文件类型
复制代码
src/
├── components/          // 通用组件
│   ├── Button/
│   │   ├── index.jsx
│   │   ├── Button.test.jsx
│   │   └── Button.module.css
│   └── Modal/
├── features/            // 按业务功能组织
│   ├── user/
│   │   ├── components/  // user 功能专用组件
│   │   ├── hooks/       // user 相关自定义 Hook
│   │   ├── services/    // user 相关 API
│   │   ├── types.ts     // user 相关类型
│   │   └── index.ts
│   └── order/
├── hooks/               // 全局通用自定义 Hook
├── utils/               // 工具函数
├── services/            // API 请求封装
├── store/               // 全局状态
├── styles/              // 全局样式
└── App.jsx
代码规范:ESLint + Prettier + Husky
json 复制代码
// package.json
{
  "scripts": {
    "lint": "eslint src --ext .js,.jsx,.ts,.tsx",
    "format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,css,md}\"",
    "prepare": "husky install"
  },
  "lint-staged": {
    "*.{js,jsx,ts,tsx}": ["eslint --fix", "prettier --write"],
    "*.{css,md}": ["prettier --write"]
  }
}

七、竞品对比:四大主流状态管理方案横向对比

在企业项目中,选择合适的状态管理方案至关重要。下面是四种主流方案的详细对比:

对比维度 Redux Toolkit Zustand Jotai Context API
核心理念 集中式单一数据源,Action + Reducer 极简集中式 Store,Hook 优先 原子化状态,自底向上组合 内置跨层级依赖注入
包体积 较大(约 15KB gzip) 极小(约 1KB gzip) 小(约 2KB gzip) 0(React 内置)
学习曲线 较陡,概念多 平缓,几乎零学习成本 中等,需要理解原子概念 低,React 基础即可
样板代码 RTK 已大幅减少,但仍有一定量 极少 少 少
性能表现 优秀,useSelector 精确订阅 优秀,天然支持选择器 极佳,细粒度原子更新 一般,Context 变化全量重渲染
调试工具 Redux DevTools,时间旅行调试 有 DevTools 中间件 有 DevTools 无专用工具
中间件生态 极其丰富(thunk、saga、observable) 有一定生态 较少 无
TypeScript 支持 优秀 优秀 优秀 优秀
适用场景 大型企业级应用,复杂状态流转 中小项目,追求简洁开发体验 细粒度响应式需求,派生状态多 低频变化的全局配置(主题、权限、语言)
不适合场景 小项目,简单状态 超大型项目需要强约束时 状态高度集中的场景 高频变化的全局状态

选型建议

  1. 大型企业项目(10人以上团队):优先选 Redux Toolkit,规范和约束能保证团队协作的一致性
  2. 中小型项目:Zustand 是最佳选择,API 极简,开发体验极佳
  3. 细粒度响应式需求:比如复杂的表单、图表联动,Jotai 的原子化模型更合适
  4. 简单全局配置:主题、语言、用户信息这类低频变化的数据,用 Context API 就够了,不需要引入额外库

八、常用场景教学

场景一:表单处理

企业项目中表单是最常见的场景。推荐使用 react-hook-form + zod 的组合,性能好、类型安全、验证强大。

bash 复制代码
npm install react-hook-form zod @hookform/resolvers
tsx 复制代码
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

// 定义表单验证 Schema
const loginSchema = z.object({
  email: z.string().email('请输入有效的邮箱地址'),
  password: z.string().min(6, '密码至少6位'),
  rememberMe: z.boolean().optional()
});

type LoginFormValues = z.infer<typeof loginSchema>;

function LoginForm() {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting }
  } = useForm<LoginFormValues>({
    resolver: zodResolver(loginSchema),
    defaultValues: {
      email: '',
      password: '',
      rememberMe: false
    }
  });

  const onSubmit = async (data: LoginFormValues) => {
    try {
      await api.login(data);
      navigate('/dashboard');
    } catch (error) {
      // 处理错误
    }
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <div>
        <label>邮箱</label>
        <input type="email" {...register('email')} />
        {errors.email && <span className="error">{errors.email.message}</span>}
      </div>

      <div>
        <label>密码</label>
        <input type="password" {...register('password')} />
        {errors.password && <span className="error">{errors.password.message}</span>}
      </div>

      <label>
        <input type="checkbox" {...register('rememberMe')} />
        记住我
      </label>

      <button type="submit" disabled={isSubmitting}>
        {isSubmitting ? '登录中...' : '登录'}
      </button>
    </form>
  );
}

场景二:数据请求与缓存

服务端状态不要和客户端状态混在一起管理。推荐使用 TanStack Query(原 React Query)来处理数据请求、缓存、重试、失效等。

bash 复制代码
npm install @tanstack/react-query
tsx 复制代码
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';

// 查询数据
function useUserList() {
  return useQuery({
    queryKey: ['users'],
    queryFn: () => api.getUsers(),
    staleTime: 5 * 60 * 1000, // 5分钟内认为数据是新鲜的
    retry: 3 // 失败重试3次
  });
}

// 修改数据
function useCreateUser() {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (userData) => api.createUser(userData),
    onSuccess: () => {
      // 创建成功后使用户列表缓存失效,自动重新获取
      queryClient.invalidateQueries({ queryKey: ['users'] });
    }
  });
}

function UserManagement() {
  const { data: users, isLoading, error } = useUserList();
  const { mutate: createUser, isPending } = useCreateUser();

  if (isLoading) return <Loading />;
  if (error) return <Error message={error.message} />;

  return (
    <div>
      <button onClick={() => createUser({ name: '新用户' })} disabled={isPending}>
        添加用户
      </button>
      <ul>
        {users.map(user => (
          <li key={user.id}>{user.name}</li>
        ))}
      </ul>
    </div>
  );
}

场景三:权限控制

企业应用通常需要精细的权限控制。通过自定义 Hook + 高阶组件的方式,可以优雅地实现权限判断。

tsx 复制代码
// 权限 Hook
function usePermission() {
  const { user } = useUser();

  const hasPermission = useCallback((permission: string) => {
    if (!user) return false;
    return user.permissions.includes(permission);
  }, [user]);

  return { hasPermission, user };
}

// 权限控制组件
function PermissionGuard({
  permission,
  fallback = null,
  children
}: {
  permission: string;
  fallback?: React.ReactNode;
  children: React.ReactNode;
}) {
  const { hasPermission } = usePermission();

  if (!hasPermission(permission)) {
    return <>{fallback}</>;
  }

  return <>{children}</>;
}

// 使用示例
function Dashboard() {
  return (
    <div>
      <h1>仪表盘</h1>

      {/* 只有有用户管理权限的人才能看到 */}
      <PermissionGuard permission="user:manage">
        <UserManagementPanel />
      </PermissionGuard>

      {/* 没有权限时显示提示 */}
      <PermissionGuard
        permission="report:export"
        fallback={<div className="no-permission">您没有导出权限</div>}
      >
        <ExportButton />
      </PermissionGuard>
    </div>
  );
}

九、企业项目实战:如何落地最佳实践

第一步:建立规范文档

在项目初期就制定好团队规范,包括:

  • 组件命名规范(PascalCase,功能+类型,如 UserList、LoginForm)
  • 自定义 Hook 命名规范(use 开头,如 useFetch、useDebounce)
  • 目录结构规范
  • 状态管理选型和使用规范
  • 样式方案(CSS Modules / styled-components / Tailwind)

第二步:配置自动化工具

bash 复制代码
# 代码检查和格式化
npm install eslint prettier --save-dev

# Git 提交前检查
npm install husky lint-staged --save-dev

# 提交信息规范
npm install commitizen cz-conventional-changelog --save-dev
javascript 复制代码
// .eslintrc.js
module.exports = {
  extends: [
    'react-app',
    'react-app/jest',
    'plugin:react-hooks/recommended'
  ],
  rules: {
    'react-hooks/rules-of-hooks': 'error',
    'react-hooks/exhaustive-deps': 'warn',
    'no-console': ['warn', { allow: ['warn', 'error'] }]
  }
};

第三步:封装通用能力

在项目中沉淀通用的组件和 Hook:

复制代码
src/
├── components/
│   ├── ProTable/       // 通用表格(搜索+分页+操作)
│   ├── ProForm/        // 通用表单
│   ├── Modal/          // 通用弹窗
│   └── Empty/          // 空状态
└── hooks/
    ├── useDebounce.js  // 防抖
    ├── useThrottle.js  // 节流
    ├── useLocalStorage.js // 本地存储
    └── usePagination.js   // 分页逻辑

第四步:Code Review 检查清单

每次 Code Review 时,对照以下清单检查:

  • 组件是否遵循单一职责?
  • 状态是否放在了合适的层级?
  • 是否有冗余状态?
  • Hooks 依赖数组是否完整?
  • 是否有不必要的 useMemo / useCallback?
  • Props 是否有类型定义?
  • 列表是否有正确的 key?
  • 是否有直接修改状态的情况?

十、面试官高频面试题

题目一:React 中为什么不能直接修改 state?

参考答案:

React 通过比较状态的引用来判断是否需要重新渲染(使用 Object.is 进行浅比较)。如果直接修改原对象,引用没有变化,React 就不会触发重新渲染,导致 UI 和状态不一致。

另外,不可变更新也使得状态变化可追溯,配合 Redux DevTools 可以实现时间旅行调试。

题目二:useEffect 的依赖数组如果漏写会有什么问题?怎么避免?

参考答案:

如果依赖数组漏写了回调中使用的响应式值,effect 就不会在这些值变化时重新执行,导致使用的是旧值(闭包陷阱)。

避免方法:

  1. 安装并配置 eslint-plugin-react-hooks,它会自动检查依赖完整性
  2. 对于复杂逻辑,考虑把逻辑抽取到自定义 Hook 中
  3. 使用函数式更新(setState(prev => prev + 1))避免把 state 放进依赖

题目三:React.memo、useMemo、useCallback 有什么区别?分别在什么场景使用?

参考答案:

工具 作用 使用场景
React.memo 缓存组件,props 不变时跳过渲染 纯展示组件,且父组件频繁重渲染
useMemo 缓存计算结果 昂贵的计算(大数组过滤、排序、转换)
useCallback 缓存函数引用 函数作为 props 传给 memo 化的子组件

注意:这三者都不是必须的,滥用反而会增加内存消耗和代码复杂度。应该先通过 Profiler 找到性能瓶颈,再有针对性地使用。

题目四:Context API 有什么性能问题?如何优化?

参考答案:

Context 的问题是:只要 Provider 的 value 发生变化,所有消费这个 Context 的组件都会重新渲染,不管它们用到的那部分值有没有变。

优化方法:

  1. 拆分 Context:按领域拆成多个小 Context,减少影响范围
  2. 稳定 value 引用 :用 useMemo 缓存 context value,避免每次渲染创建新对象
  3. 使用选择器模式:封装自定义 Hook,让组件只订阅自己需要的部分
  4. 考虑使用状态管理库:Redux、Zustand、Jotai 都支持精确订阅,不会全量重渲染

题目五:如何设计一个可维护的 React 项目目录结构?

参考答案:

推荐按功能(feature)组织,而不是按文件类型组织。每个功能模块内部包含自己的组件、Hook、服务、类型和测试。

优点:

  • 高内聚低耦合,修改一个功能只需要关注一个目录
  • 便于代码查找和理解
  • 方便功能模块的复用和删除

同时,通用的组件、Hook、工具函数放在全局共享目录中。

题目六:React 18 的并发特性对最佳实践有什么影响?

参考答案:

React 18 引入了并发渲染,这意味着渲染过程可以被中断、暂停和恢复。这对最佳实践的影响:

  1. 渲染函数必须是纯函数:因为渲染可能被多次调用或中断,不能在 render 中产生副作用
  2. 副作用放在 useEffect 中:不要在渲染过程中修改外部状态
  3. 使用 useTransition 标记非紧急更新 :比如搜索过滤、标签切换,可以用 useTransition 让紧急更新(输入框)优先
  4. useDeferredValue 延迟更新:对于渲染开销大的列表,可以延迟处理新值

题目七:自定义 Hook 和普通函数有什么区别?

参考答案:

自定义 Hook 本质上就是一个函数,但它有两个特点:

  1. 命名以 use 开头,这是约定,也是 ESLint 插件识别 Hook 的依据
  2. 内部可以调用其他 Hooks(useState、useEffect 等),普通函数不能

自定义 Hook 的作用是把组件中可复用的状态逻辑抽取出来,实现逻辑的复用,而不是 UI 的复用。


十一、总结

React 最佳实践不是一成不变的教条,而是随着 React 本身的演进而不断发展的。从 Class 组件到 Hooks,从手动优化到 React Compiler,最佳实践的核心始终是:

  1. 状态清晰:状态放在该放的地方,不冗余、不矛盾
  2. 组件纯粹:单一职责,组合优于继承
  3. 性能合理:先测量再优化,不滥用记忆化
  4. 规范统一:工程化保障团队协作效率
  5. 持续演进:关注 React 官方动态,及时拥抱新特性

记住,最佳实践是手段不是目的。最终目标是写出可维护、高性能、用户体验好的应用。在实际项目中,要根据团队规模、项目复杂度、业务需求灵活选择,不要为了用而用。

希望这篇文章能帮你建立起 React 最佳实践的完整知识体系,在日常开发和面试中都能游刃有余。


转载声明:本文为原创文章,如需转载,请联系作者获得授权,并注明出处。

相关推荐
我星期八休息1 小时前
Redis 的原理和使用(2)
前端·数据库·redis·python·mysql·缓存·bootstrap
溪语流沙1 小时前
【Web全栈进阶】文件上传与图片展示:早报站的第一张图片
前端·vue.js·python·fastapi
alwaysrun1 小时前
Web之wasm-bindgen详解
前端·wasm·bindgen
鬼手点金1 小时前
如何选择模型
java·前端·人工智能·git·深度学习
恋猫de小郭1 小时前
Dart 3.15 新特性:this 终于支持类型提升了
android·前端·flutter
IT_陈寒2 小时前
Vue开发实战:从入门到精通
前端·人工智能·后端
znnnk2 小时前
2026 前端工程化新纪元:Rust 重塑工具链,AI Agent 接管流水线
前端·人工智能·rust
王大师王文峰2 小时前
HTML 入门到进阶:语义化标签、表单与多媒体一篇打通【语义化布局、表单校验、video/audio、Canvas、无障碍与 SEO】
前端·css·node.js·html·css3·jquery·html5
风骏时光牛马2 小时前
AI_Coding:智能代码创作助手
前端