
19、ReactJS - 最佳实践(Best Practices):从入门到企业级落地的完整指南
写了三年 React,代码还是一团糟?改一个 bug 引出三个 bug?页面输入一个字卡三秒?这篇文章将系统梳理 React 最佳实践,从是什么、为什么、怎么演进、怎么用到企业级落地,配合真实代码和面试高频题,让你看完就能直接上手。
文章目录
- [19、ReactJS - 最佳实践(Best Practices):从入门到企业级落地的完整指南](#19、ReactJS - 最佳实践(Best Practices):从入门到企业级落地的完整指南)
-
- [一、痛点场景:那些年我们一起踩过的 React 坑](#一、痛点场景:那些年我们一起踩过的 React 坑)
-
- [场景一:状态满天飞,改一个 bug 引出三个 bug](#场景一:状态满天飞,改一个 bug 引出三个 bug)
- [场景二:上帝组件,一个文件 2000 行](#场景二:上帝组件,一个文件 2000 行)
- 场景三:页面卡顿,输入一个字卡三秒
- [场景四:Hooks 依赖数组地狱](#场景四:Hooks 依赖数组地狱)
- [二、痛点的解决方案:React 最佳实践全景图](#二、痛点的解决方案:React 最佳实践全景图)
- [三、是什么:React 最佳实践的专业定义](#三、是什么:React 最佳实践的专业定义)
- 四、为什么用:不遵守最佳实践会怎样
- [五、演进历程:React 最佳实践是怎么一步步走到今天的](#五、演进历程:React 最佳实践是怎么一步步走到今天的)
-
- [第一阶段:Class 组件时代(2013-2018)](#第一阶段:Class 组件时代(2013-2018))
- [第二阶段:Mixins 时代(已废弃)](#第二阶段:Mixins 时代(已废弃))
- [第三阶段:HOC 高阶组件时代(2016-2019)](#第三阶段:HOC 高阶组件时代(2016-2019))
- [第四阶段:Hooks 时代(2019 至今)](#第四阶段:Hooks 时代(2019 至今))
- [第五阶段:React Compiler 时代(2025 开启)](#第五阶段:React Compiler 时代(2025 开启))
- 六、怎么用:五大核心最佳实践实战
-
- [6.1 状态管理最佳实践](#6.1 状态管理最佳实践)
- [6.2 组件设计最佳实践](#6.2 组件设计最佳实践)
-
- 原则一:单一职责,一个组件只做一件事
- 原则二:组合优于继承
- [原则三:Props 类型校验,提前发现问题](#原则三:Props 类型校验,提前发现问题)
- [6.3 Hooks 使用规范](#6.3 Hooks 使用规范)
-
- [规则一:只在顶层调用 Hooks](#规则一:只在顶层调用 Hooks)
- [规则二:只在 React 函数中调用 Hooks](#规则二:只在 React 函数中调用 Hooks)
- [规则三:依赖数组要完整,不要欺骗 React](#规则三:依赖数组要完整,不要欺骗 React)
- [规则四:自定义 Hook 抽取可复用逻辑](#规则四:自定义 Hook 抽取可复用逻辑)
- [6.4 性能优化最佳实践](#6.4 性能优化最佳实践)
-
- 原则一:先测量再优化,不要过早优化
- [原则二:合理使用 React.memo](#原则二:合理使用 React.memo)
- [原则三:useMemo 缓存昂贵计算](#原则三:useMemo 缓存昂贵计算)
- [原则四:useCallback 稳定函数引用](#原则四:useCallback 稳定函数引用)
- [原则五:拆分 Context,避免全局重渲染](#原则五:拆分 Context,避免全局重渲染)
- 原则六:长列表虚拟化
- [6.5 工程化最佳实践](#6.5 工程化最佳实践)
-
- 目录结构:按功能组织,不要按文件类型
- [代码规范:ESLint + Prettier + Husky](#代码规范:ESLint + Prettier + Husky)
- 七、竞品对比:四大主流状态管理方案横向对比
- 八、常用场景教学
- 九、企业项目实战:如何落地最佳实践
-
- 第一步:建立规范文档
- 第二步:配置自动化工具
- 第三步:封装通用能力
- [第四步:Code Review 检查清单](#第四步:Code Review 检查清单)
- 十、面试官高频面试题
-
- [题目一:React 中为什么不能直接修改 state?](#题目一:React 中为什么不能直接修改 state?)
- [题目二:useEffect 的依赖数组如果漏写会有什么问题?怎么避免?](#题目二:useEffect 的依赖数组如果漏写会有什么问题?怎么避免?)
- [题目三:React.memo、useMemo、useCallback 有什么区别?分别在什么场景使用?](#题目三:React.memo、useMemo、useCallback 有什么区别?分别在什么场景使用?)
- [题目四:Context API 有什么性能问题?如何优化?](#题目四:Context API 有什么性能问题?如何优化?)
- [题目五:如何设计一个可维护的 React 项目目录结构?](#题目五:如何设计一个可维护的 React 项目目录结构?)
- [题目六:React 18 的并发特性对最佳实践有什么影响?](#题目六:React 18 的并发特性对最佳实践有什么影响?)
- [题目七:自定义 Hook 和普通函数有什么区别?](#题目七:自定义 Hook 和普通函数有什么区别?)
- 十一、总结
一、痛点场景:那些年我们一起踩过的 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 应用开发过程中,经过社区大量项目验证、能够显著提升代码质量、可维护性、性能和开发效率的一系列编码规范、设计模式和架构原则的集合。
它主要包含五大核心领域:
- 状态管理:合理组织和管理应用状态,避免冗余和矛盾
- 组件设计:遵循单一职责原则,通过组合而非继承构建 UI
- Hooks 规范:正确使用 Hooks,遵守调用规则和依赖管理
- 性能优化:减少不必要的渲染,提升应用响应速度
- 工程化:目录结构、代码规范、类型检查、测试等配套设施
大白话
说白了,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 支持 | 优秀 | 优秀 | 优秀 | 优秀 |
| 适用场景 | 大型企业级应用,复杂状态流转 | 中小项目,追求简洁开发体验 | 细粒度响应式需求,派生状态多 | 低频变化的全局配置(主题、权限、语言) |
| 不适合场景 | 小项目,简单状态 | 超大型项目需要强约束时 | 状态高度集中的场景 | 高频变化的全局状态 |
选型建议
- 大型企业项目(10人以上团队):优先选 Redux Toolkit,规范和约束能保证团队协作的一致性
- 中小型项目:Zustand 是最佳选择,API 极简,开发体验极佳
- 细粒度响应式需求:比如复杂的表单、图表联动,Jotai 的原子化模型更合适
- 简单全局配置:主题、语言、用户信息这类低频变化的数据,用 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 就不会在这些值变化时重新执行,导致使用的是旧值(闭包陷阱)。
避免方法:
- 安装并配置
eslint-plugin-react-hooks,它会自动检查依赖完整性 - 对于复杂逻辑,考虑把逻辑抽取到自定义 Hook 中
- 使用函数式更新(
setState(prev => prev + 1))避免把 state 放进依赖
题目三:React.memo、useMemo、useCallback 有什么区别?分别在什么场景使用?
参考答案:
| 工具 | 作用 | 使用场景 |
|---|---|---|
React.memo |
缓存组件,props 不变时跳过渲染 | 纯展示组件,且父组件频繁重渲染 |
useMemo |
缓存计算结果 | 昂贵的计算(大数组过滤、排序、转换) |
useCallback |
缓存函数引用 | 函数作为 props 传给 memo 化的子组件 |
注意:这三者都不是必须的,滥用反而会增加内存消耗和代码复杂度。应该先通过 Profiler 找到性能瓶颈,再有针对性地使用。
题目四:Context API 有什么性能问题?如何优化?
参考答案:
Context 的问题是:只要 Provider 的 value 发生变化,所有消费这个 Context 的组件都会重新渲染,不管它们用到的那部分值有没有变。
优化方法:
- 拆分 Context:按领域拆成多个小 Context,减少影响范围
- 稳定 value 引用 :用
useMemo缓存 context value,避免每次渲染创建新对象 - 使用选择器模式:封装自定义 Hook,让组件只订阅自己需要的部分
- 考虑使用状态管理库:Redux、Zustand、Jotai 都支持精确订阅,不会全量重渲染
题目五:如何设计一个可维护的 React 项目目录结构?
参考答案:
推荐按功能(feature)组织,而不是按文件类型组织。每个功能模块内部包含自己的组件、Hook、服务、类型和测试。
优点:
- 高内聚低耦合,修改一个功能只需要关注一个目录
- 便于代码查找和理解
- 方便功能模块的复用和删除
同时,通用的组件、Hook、工具函数放在全局共享目录中。
题目六:React 18 的并发特性对最佳实践有什么影响?
参考答案:
React 18 引入了并发渲染,这意味着渲染过程可以被中断、暂停和恢复。这对最佳实践的影响:
- 渲染函数必须是纯函数:因为渲染可能被多次调用或中断,不能在 render 中产生副作用
- 副作用放在 useEffect 中:不要在渲染过程中修改外部状态
- 使用 useTransition 标记非紧急更新 :比如搜索过滤、标签切换,可以用
useTransition让紧急更新(输入框)优先 - useDeferredValue 延迟更新:对于渲染开销大的列表,可以延迟处理新值
题目七:自定义 Hook 和普通函数有什么区别?
参考答案:
自定义 Hook 本质上就是一个函数,但它有两个特点:
- 命名以
use开头,这是约定,也是 ESLint 插件识别 Hook 的依据 - 内部可以调用其他 Hooks(useState、useEffect 等),普通函数不能
自定义 Hook 的作用是把组件中可复用的状态逻辑抽取出来,实现逻辑的复用,而不是 UI 的复用。
十一、总结
React 最佳实践不是一成不变的教条,而是随着 React 本身的演进而不断发展的。从 Class 组件到 Hooks,从手动优化到 React Compiler,最佳实践的核心始终是:
- 状态清晰:状态放在该放的地方,不冗余、不矛盾
- 组件纯粹:单一职责,组合优于继承
- 性能合理:先测量再优化,不滥用记忆化
- 规范统一:工程化保障团队协作效率
- 持续演进:关注 React 官方动态,及时拥抱新特性
记住,最佳实践是手段不是目的。最终目标是写出可维护、高性能、用户体验好的应用。在实际项目中,要根据团队规模、项目复杂度、业务需求灵活选择,不要为了用而用。
希望这篇文章能帮你建立起 React 最佳实践的完整知识体系,在日常开发和面试中都能游刃有余。
转载声明:本文为原创文章,如需转载,请联系作者获得授权,并注明出处。