“rea”这个词,在很多开发者眼里其实就指React——准确说,是工作群里有人把单词拼错了,然后大家顺手沿用下来的叫法。我在多个项目里见过这种缩写,时间长了反而觉得它挺亲切。这篇文章就从“rea”出发,把React最值得花时间搞懂的核心东西重新梳理一遍:组件化思路、状态管理、Hooks的底层逻辑,以及我踩过的那些真坑。不管你是刚读完官方文档的新手,还是已经写了半年业务代码但总觉得哪里没通透的初中级前端,这篇文章的方向都会适合你。我会尽量从“为什么要这么做”的角度去讲,而不是只丢一堆语法规则。
1. 项目概述:React到底在解决什么问题
1.1 从“rea”这个缩写说起
先说个有意思的现象。很多团队在沟通时会把React顺手写成“rea”,就像有人会把JavaScript写成“js”、把TypeScript写成“ts”一样。这个缩写能在不少内部文档、需求描述甚至技术方案里看到,说明它已经成了一个约定俗成的黑话。初看这个标题时可能觉得莫名其妙,但在实际开发语境里,“rea”基本默认指向React及其生态。
为什么要专门写React?因为它是目前前端领域使用面最广的UI框架之一。国内外的招聘要求、开源项目、企业级中后台系统,React都占据着极高的比例。即使你没有在用它,也可能因为接手旧项目、阅读开源代码或多端框架选型而绕不开它。理解React的核心概念,意味着你能读懂大量现代前端项目的代码组织思路。
1.2 React能做什么,不能做什么
React解决的核心问题是UI状态与界面的同步。在早期前端开发中,操作DOM是一件繁琐且容易出错的事情:你需要手动找到元素、修改属性、更新样式、处理事件绑定。当页面规模变大,这种命令式编程方式的缺陷就会被无限放大——一个数据变化可能要牵动多个DOM节点的手动更新,代码之间耦合严重,维护成本呈指数级上升。
React用“组件”的方式把界面拆成独立单元,把“界面长什么样”变成“数据状态的函数”。你只需要描述:在某个状态下,界面应该展示成什么样。React负责在状态变化时自动找出最小的DOM差异并完成更新。这种声明式思维大大降低了前端开发的复杂度,也让团队协作时每个人只需要关注自己承担的组件部分。
但它不解决所有问题。React本身只是一个UI库,路由、数据请求、状态管理、表单验证这些能力都需要搭配生态里的其他方案。很多初学者的困惑正是“不知道怎么把这些工具串起来”,所以这篇文章也会顺带讲讲组件之间如何通信、状态该放在哪一层这样的架构问题。
1.3 适合谁来读这篇文章
我希望读者是两类人:一是刚刚接触React、写过一点组件但总觉得概念零散的新手;二是已经写了一阵业务,却总在状态更新不生效、组件频繁渲染这类问题上卡壳的开发者。虽然文章不会从头教JavaScript语法,但每个关键概念我都会解释“为什么会有这个东西”以及“它对应解决了什么痛点”,这样即使基础一般的人也能跟上节奏。
2. 组件化设计的拆解思路
2.1 为什么组件是React的基石
组件化的价值用一句话讲:把复杂问题拆分成若干个简单问题,每个组件负责自己的那一块。这跟写文章是一个道理——一篇长文如果全堆在一个段落里,读者看着头大;拆成标题、小节、列表,结构清晰了,信息也就更容易消化。
在React里,你可以通过一个函数或一个类来定义组件。函数组件接收props参数,返回React元素描述界面。这个设计使得组件成为一个“纯函数”:同样的输入,永远返回同样的输出。这个纯函数的特性非常重要,它让组件变得容易测试、容易推理,也让React能够在内部做性能优化——如果一个组件的props没有变化,React就可以跳过它的渲染过程。
实际开发中我习惯把组件按职责分层。最底下是基础组件,比如按钮、输入框、弹窗这类通用元素;中间是业务组件,比如用户卡片、订单列表,它们把多个基础组件组合在一起;最上层是页面组件,对应具体的路由地址,负责拉取数据并把数据分发到业务组件。这不是什么框架规定,而是我从多个项目中总结出的经验,能有效减少组件之间逻辑纠缠的问题。
2.2 props:组件之间的数据通道
props是父组件向子组件传递数据的通道。它从设计上保证了数据流向是单向的:父组件拥有数据,子组件通过props接收数据并展示。这个单向数据流的概念很重要,因为它让数据的来源变得可追溯,调试的时候能顺着组件树一路查下去,效率会高很多。
按我个人习惯,props的命名尽量使用语义明确的词,布尔值用is或has开头(比如isLoading、hasError),回调函数用on开头(比如onClick、onSave)。这样做的好处是,团队里其他人不需要翻父组件代码就能猜出props的用途。一份清晰的props约定,相当于给组件画了一份简易文档。
值得注意的一个细节是:props应当被当作只读数据来对待。子组件不应该直接修改props,因为你修改的是父组件拥有的数据,这会造成数据来源混乱。遇到需要变更数据的情况,正确做法是调用父组件通过props传下来的回调函数(比如onXxx),由父组件执行真正的状态更新。这个规则在React官方文档里被反复强调,但它也是初学者最爱犯的错误之一。
2.3 状态设计:数据该放哪儿
状态的设计往往决定了一个组件是“好维护”还是“改一处崩三处”。谈状态设计前,得先明确一个原则:能由别人管的数据就不要自己管。如果某份数据是页面级共享的,应该放在页面组件里;如果只是一处下拉框当前选中了什么值,那就在该位置组件内部用useState管理就够了。
我在项目实操里常按“数据来源”划分状态类型:
- 来自后端接口的数据:一般在请求时放入组件状态或全局状态池,由服务端数据的变化驱动界面刷新。
- 来自用户操作的数据:表单输入值、开关状态、弹窗的显示隐藏,这类状态生命周期短,适合放在离使用位置最近的组件里。
- 来自计算派生的数据:通过已有数据计算出的结果,例如列表过滤后的展示列表。
很多人习惯什么东西都塞进useState,结果组件里塞了十几个状态变量,上下逻辑交错,连自己看着都头疼。更好的做法是:先看这个数据是不是能通过已有状态推导出来,如果是,就不该单独存一份。比如一个待办列表todos和一个已完成数量completedCount,后者完全可以通过todos.filter(t => t.done).length计算得到,完全没有必要再开一个状态来维护它,否则你在修改todos时还要记得同步修改completedCount,这是典型的埋坑行为。
3. 核心API深度解析:useState、useEffect、useRef
3.1 useState:让函数组件拥有记忆
在React 16.8之前,类组件是唯一能拥有状态的方式,代码里到处都是this的绑定和生命周期函数,理解成本并不低。Hooks的推出把状态能力带给了函数组件,useState就是其中最基础的那个。
import { useState } from 'react'; function Counter() { const [count, setCount] = useState(0); const handleClick = () => { setCount(count + 1); }; return ( <div> <p>当前值:{count}</p> <button onClick={handleClick}>加一</button> </div> ); }useState返回一个数组:第一个元素是当前状态值,第二个是更新函数。数组解构的写法很常见,但很多人没想过,为什么不返回一个对象?如果返回对象{ value, setValue },每次使用都要自定义变量名并手动解构,而数组解构可以直接这样写const [count, setCount] = useCount(),使用起来更加简洁。
关于setState需要特别强调的是:状态的更新是异步的、批量的。React会在事件处理函数中自动批量更新状态,也就是如果在一次点击里连续调用三次setCount(count + 1),count只会加1而不是加3,因为三次调用使用的都是同一个闭包里的旧值。正确的写法是传入函数式更新setCount(prev => prev + 1),这样React会依次把上一次的结果传给下次更新。
这是一个极其常见的坑,我记得自己第一次遇到的时候排查了大半天,最后发现是批量更新机制在起作用。带着这个认知去看很多“状态没更新”的问题,思路会清晰很多。
3.2 useEffect:管理副作用和执行时机
useEffect用来在组件渲染后执行副作用操作,包括数据请求、订阅事件、手动修改DOM、设置定时器等。它把原本分散在类组件多个生命周期方法里的逻辑,集中到一个统一的地方。
import { useEffect, useState } from 'react'; function UerList({ userId }) { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { let cancelled = false; setLoading(true); fetch(`/api/users/${userId}`) .then(res => res.json()) .then(data => { if (!cancelled) { setUser(data); setLoading(false); } }); return () => { cancelled = true; }; }, [userId]); if (loading) return <div>加载中...</div>; return ( <div> <span>姓名:{user.name}</span> <span>邮箱:{user.email}</span> </div> ); }依赖数组[userId]的作用是告诉React:只有当userId变化时,这个effect才需要重新执行。这里有个初学者必须理解的点:如果依赖数组里的值在effect中被使用,那它就必须被写进依赖数组,否则会出现闭包捕获旧值的bug。React官方提供了eslint-plugin-react-hooks的exhaustive-deps规则来自动检查这一点,我强烈建议在项目里开启它,它会帮你在开发效率上省下大量时间。
我后来在写类似逻辑时,一定会在effect开头设置一个let cancelled = false并配合cleanup函数把它置为true。这段代码的作用是防止组件卸载后状态被更新的警告——在快速切换列表项的交互中,该警告尤为频繁。很多老教程里没有这一写法,但它确实是实战中很重要的细节。
3.3 useRef:不引起渲染的“可变盒子”
useRef看起来像个普通的容器,但它有一个其他状态工具没有的特性:修改ref的值不会触发组件重新渲染。也就是说,你可以用它保存任意可变数据,并在组件整个生命周期中随时读取最新值,而不必担心多余的性能消耗。
使用场景最典型的有三个:一是访问DOM节点,比如让输入框自动聚焦;二是在定时器或事件回调里保存最新状态值;三是在effect之间传递数据。不过要记住,操作DOM的代码通常应该放在useEffect而不是直接写在事件回调里,因为DOM挂在真实页面上的时机是由React控制的。
import { useRef, useEffect } from 'react'; function AutoFocusInput() { const inputRef = useRef(null); useEffect(() => { inputRef.current?.focus(); }, []); return <input ref={inputRef} placeholder="加载后自动聚焦" />; }inputRef.current?.focus()这个写法里可选链操作符是我的习惯,它避免了对null的显式判断,代码更简洁。useRef还有一类用途常见但容易忽略——保存上一个渲染的值:用ref记录prevProps.x,在渲染期间与当前props.x比较,就能判断出值是从什么变成什么,这种模式在实现数据变化动画或日志分析时很有用。
3.4 自定义Hook:把逻辑抽出来复用
当你发现两处组件都在做类似的事(比如都用fetch请求数据、都管理loading状态、都在卸载时取消请求),就该考虑抽一个自定义Hook了。自定义Hook本质上就是一个以use开头的函数,它内部可以使用其他内置Hook。
function useFetch(url) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); useEffect(() => { const controller = new AbortController(); setLoading(true); fetch(url, { signal: controller.signal }) .then(res => res.json()) .then(setData) .finally(() => setLoading(false)); return () => controller.abort(); }, [url]); return { data, loading }; }这个自定义Hook把数据请求的整个流程封装起来,用户列表组件只需要调用const { data, loading } = useFetch('/api/users'),三行代码就拿到了数据。第一个版本并没有用AbortController,但在实际项目中快速切换路由时经常遇到“请求完成但组件已经卸载”的报错信息,后来加入AbortController并在cleanup里调用abort后,一切安静了。
自定义Hook的价值在于它把组件渲染逻辑与业务逻辑解耦,组件变得更薄、更专注、更容易测试。我在多个项目中维护过一批自定义Hook,它们就像是团队内部的“逻辑工具库”,日积月累复用率很高。
4. 从零搭建一个React实战项目
4.1 快速初始化工程
读了很多文档,不亲手敲一遍代码等于白看。这一节我会带你把一个完整的React小项目跑起来。让我们做一个“用户卡片管理面板”,功能包括:展示用户列表、点击用户查看详情、支持搜索过滤。这个项目虽然简单,但它会串起组件拆分、状态提升、数据请求、条件渲染等多个核心概念。
首先用Node.js自带的构建工具创建项目。Node版本建议在18或以上,更高的版本对应更快的构建速度与更新特性。
npm create vite@latest user-card-panel -- --template react用Vite来搭建是因为它的启动速度快、配置简单,对新手更友好。生成完项目后进入目录,安装依赖:
cd user-card-panel npm install npm run dev运行npm run dev后,浏览器打开终端提示的本地地址,就能看到Vite默认的欢迎页面。我第一次用Vite时最直观的感受是热更新太快了,几乎保存完代码界面立即刷新,对开发体验的提升立竿见影。
4.2 组织组件目录结构
我习惯按功能而非按文件类型来组织目录。一个简单的规则是:所有组件放在src/components下,再按业务域拆分子目录,每个组件自己的样式文件放在同目录下。这样某个功能相关的代码都在一起,要删要改都方便。
src/ ├── components/ │ ├── UserCard/ │ │ ├── index.jsx │ │ └── styles.css │ ├── UserDetail/ │ │ ├── index.jsx │ │ └── styles.css │ └── SearchBar/ │ ├── index.jsx │ └── styles.css ├── hooks/ │ └── useFetch.js ├── App.jsx └── main.jsx这种结构不是某种官方规范,而是项目演进下来我觉得最顺手的组织方式。它有很多好处,其中最重要的一个:当你需要修改用户卡片相关代码时,只需要打开一个目录,不需要在十几个文件夹之间来回跳转。
4.3 从静态组件到状态联动
先实现最简单的静态版本:一个UserCard组件展示某个用户的名字和邮箱。这能帮助你快速建立“组件输出界面”的直觉。
function UserCard({ user, onSelect }) { return ( <div className="user-card" onClick={() => onSelect(user)}> <h3>{user.name}</h3> <p>{user.email}</p> </div> ); }接下来是SearchBar和UserDetail组件:
function SearchBar({ keyword, onKeywordChange }) { return ( <input type="text" placeholder="按姓名搜索..." value={keyword} onChange={(e) => onKeywordChange(e.target.value)} /> ); } function UserDetail({ user }) { if (!user) { return <p className="placeholder">请选择一位用户查看详情</p>; } return ( <div className="user-detail"> <h2>{user.name}</h2> <p>邮箱:{user.email}</p> <p>电话:{user.phone}</p> <p>公司:{user.company.name}</p> </div> ); }上面代码里有一个细节值得注意:UserDetail把if (!user)的判断放在了组件内部,这其实就是条件渲染的一种——数据不存在时展示占位提示,存在时展示实体内容。在实际开发中这种模式很常见,但很多初学者习惯在父组件里判断,导致父组件逻辑越来越重。把每个组件的边界划分清楚,组件才真正具有独立性。
4.4 在App.jsx中完成状态管理闭环
现在把所有组件组合到App中。这里的核心架构思路是“状态提升”:把搜索关键词和当前选中的用户放到App组件里管理,子组件通过props接收和回调。
import { useState, useMemo } from 'react'; import UserCard from './components/UserCard'; import UserDetail from './components/UserDetail'; import SearchBar from './components/SearchBar'; import { useFetch } from './hooks/useFetch'; export default function App() { const { data: users, loading } = useFetch('/api/users'); const [keyword, setKeyword] = useState(''); const [selectedUser, setSelectedUser] = useState(null); const filteredUsers = useMemo(() => { if (!users) return []; return users.filter(user => user.name.toLowerCase().includes(keyword.trim().toLowerCase()) ); }, [users, keyword]); if (loading) return <div>数据加载中...</div>; return ( <div className="app-container"> <SearchBar keyword={keyword} onKeywordChange={setKeyword} /> <div className="app-body"> <div className="user-list"> {filteredUsers.map(user => ( <UserCard key={user.id} user={user} onSelect={setSelectedUser} /> ))} </div> <UserDetail user={selectedUser} /> </div> </div> ); }这里面的useMemo很关键:它接收一个计算函数和依赖数组,只有在users或keyword变化时才重新计算过滤结果。没有它,每次渲染都会重新遍历整个列表做一次过滤。虽然在小项目里性能影响不大,但这种“把不必要的重复计算缓存起来”的意识,对构建大型应用来说是必须具备的。
组件的key={user.id}同样重要。React需要通过key来识别列表中的每一项,key稳定且唯一能帮助React在新增、删除、排序时准确复用已有DOM节点。如果图省事用数组的index来做key,当列表顺序变化时,很容易出现组件状态错乱的问题。
4.5 数据请求的接入与模拟
真实项目中,/api/users会被替换成后端真实地址。我这里使用一个模拟接口来演示:在项目根目录下创建一个mockServer.js,用Node几十行代码起一个本地服务,返回一份假数据。
import http from 'http'; const users = [ { id: 1, name: '张三', email: 'zhangsan@example.com', phone: '138-0000-0001', company: { name: '某科技公司' } }, { id: 2, name: '李四', email: 'lisi@example.com', phone: '138-0000-0002', company: { name: '某网络公司' } }, { id: 3, name: '王五', email: 'wangwu@example.com', phone: '138-0000-0003', company: { name: '某设计工作室' } } ]; http.createServer((req, res) => { res.setHeader('Access-Control-Allow-Origin', '*'); if (req.url === '/api/users') { res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify(users)); } else { res.statusCode = 404; res.end(''); } }).listen(3001, () => console.log('Mock server running at http://localhost:3001'));然后在useFetch中把请求地址指向http://localhost:3001/api/users。因为我们的前端服务和mock服务跑在不同端口,跨域问题的处理方式是后端设置允许跨域请求的响应头。实际生产环境通常由反向代理或后端配置解决,这个稍作了解即可。
我第一次跑这样的前后端分离开发环境时,就因为跨域报错卡住了很久。后来理解了CORS的机制才明白:跨域是浏览器的安全策略,不是服务器拒绝请求——浏览器发起请求后,服务器虽然正常返回了数据,但浏览器检查响应头里没有允许跨域的字段,便主动拦截掉了。理解了这一层,后续再做接口联调就会少很多困惑。
4.6 效果预览与代码走查
全部完成后,页面应该具备三个基本交互:输入搜索关键字时列表实时变化;点击用户卡片时右侧详情区域显示对应用户信息;联网失败时能通过控制台看到请求报错信息。
代码走查时我会按以下顺序检查:
- 组件职责是否单一:App有没有承担过多细节。
- props传递是否清晰:每层组件的数据接口是否明确。
- 状态是否冗余:有没有可以派生却不该单独维护的数据。
- 是否有内存泄漏风险:effect里是否都做了清理操作。
如果检查出问题,第一时间整理进项目的README中,方便后续接手的人快速理解设计意图。
5. 常见问题与报错排查记录
5.1 状态更新后界面不刷新
这是新人遇到最多的情况,我整理出最常见的几个原因:
| 症状 | 典型原因 | 解决思路 |
|---|---|---|
| 调用了setState但界面无变化 | 直接修改了state对象本身 | 创建新对象再赋值,保证引用变化 |
| 多次setState只生效一次 | 批量更新机制,使用旧值 | 使用函数式更新setState(prev => ...) |
| 子组件props变了但没重新渲染 | 依赖数组漏写了该props | 检查useEffect的依赖数组 |
| 页面永远显示初始值 | ref更新不触发渲染 | 需要渲染的数据改用useState |
直接修改state对象本身是非常典型的错误。比如现在state是一个对象const [user, setUser] = useState({ name: '张三', age: 20 }),如果执行user.age = 21再调用setUser(user),React的浅比较会发现引用没有变化,于是直接跳过更新。正确的方式是setUser({ ...user, age: 21 })展开旧对象,覆盖新字段,产生一个全新引用。
我曾在代码评审时反复强调这一点,它不只是一个理论上的知识点,而是真实发生过无数次的线上问题:列表不刷新、详情页不变、勾选状态错乱……根源基本都是引用没变。
5.2 “Cannot read property 'map' of undefined”
这个报错翻译过来是:你尝试在一个undefined值上调用map方法。最常见的产生原因是:组件已经渲染,但数据还没有请求回来,这时候users还是null。
解决办法:在组件中做防御性判断,渲染前先检查数据是否存在。
if (!users) return <div>加载中...</div>;或者用可选链users?.map(...),后者更简洁。我个人的习惯是:数据为null时显示加载状态,为数组但长度为0时显示“暂无数据”,两者区分开,用户体验会更好——加载中和空数据是完全不同的状态,混为一谈会让用户误以为页面卡死了。
5.3 useEffect无限循环
无限制的循环请求是最让人崩溃的问题之一。它经常发生在这样的代码中:
useEffect(() => { setCount(count + 1); // 状态更新又触发渲染,又触发efffect }, [count]);上面的代码会陷入死循环:effect更新了count,count改变导致组件重新渲染,渲染后又进入effect,再次更新count……修改方式有两种:一是在依赖数组里去掉count,前提是确实不需要监听它变化后的后续操作;二是调整数据结构,让effect的执行和状态更新不形成闭环。
我排查无限循环问题时的常用手段是:在effect第一行加一个console.log,然后在浏览器控制台观察打印次数。如果打印次数一直增长,那几乎可以断定是循环依赖。这种问题在复杂组件中因为跨了多个状态而变得隐蔽,但日志定位的效率非常高。
5.4 key使用index的隐患
列表渲染时如果用index作为key,可能会遇到一个诡异现象:删除某一行后,下一行意外地保留了被删行的输入框内容。原因在于key是用来标识组件身份的,用index会破坏身份标识——删除第2行后,原本的第3行变成了新的第2行,React认为“第2个位置的组件还在”,于是复用了旧组件,包括它的内部状态。
解决方式很简单:使用业务上唯一且稳定的id作为key。如果后端没有id字段,也可以在前端生成UUID或使用组合字段(比如用户id-序号)来保证唯一性。这条经验我建议直接背下来,它在几乎任何列表相关的组件中都会用到。
5.5 组件卸载后仍调用setState
这个报错在React 18之前是Warning,在18之后的版本提示语变了,但问题依然存在:一个组件已经卸载,异步请求回来后再执行setState,导致React在内部维护一个已经不存在的组件状态。我在前面useFetch的示例中已经演示了正确做法——在effect的cleanup中取消请求。
实际开发中,axios等请求库支持AbortController,fetch也原生支持,所以处理起来并不复杂。重点是你要有这样一个意识:所有异步操作都要考虑卸载场景。这个意识在组件频繁切换的页面里尤其重要。
6. 性能优化与代码规范
6.1 从useMemo到React.memo
除了useMemo,React还提供了React.memo来避免组件在props不变时的重复渲染。它是类组件PureComponent在函数组件时代的对应物。当父组件重新渲染时,useMemo只能避免重复执行计算函数,而React.memo可以直接跳过子组件的整个渲染过程。
import { memo } from 'react'; const UserCard = memo(function UserCard({ user, onSelect }) { return ( <div className="user-card" onClick={() => onSelect(user)}> <h3>{user.name}</h3> <p>{user.email}</p> </div> ); });这样只有在user或onSelect变化时,UserCard才会重新渲染。注意,如果父组件在每次渲染时都重新定义一个内联函数传给子组件,那memo的优化就要打折扣,因为props里的引用一直在变。我的做法是,传给memo化子组件的回调函数优先使用useCallback包裹,二者搭配效果更好。
一上来就在所有组件上都包memo并非最优解。优先把它用在渲染开销大、更新频繁组件列表中的稳定项上。任何优化都要基于真实的渲染瓶颈进行,而不是盲目套用。
6.2 代码规范与团队协作
代码规范这件事,单靠靠人的自觉是不现实的。我所在的项目组从一开始就引入ESLint和Prettier,提交前强制检查代码规范。有一些插件我评估后觉得有实际价值:eslint-plugin-react-hooks能自动检查Hooks的使用是否符合规则,eslint-plugin-jsx-a11y能检查JSX中的可访问性问题,后者对后期做无障碍支持会省下大量排查时间。
组件命名方面我偏好使用完整单词而非缩写,比如UserCard而不是UC、LoadingSpinner而不是LS。完整的命名在大型代码库中能显著降低检索成本。CSS类名使用项目约定的前缀避免冲突,业务上高频复用的样式抽成常量管理。这些细节单看都很小,但加起来就是代码库长期可维护性的保证。
7. 附:个人经验与踩坑总结
React这套工具没有表面上那么简单,但也没有传说中那么难。把它真正用好,需要的不是记住多少个API,而是理解它看待界面的方式——界面是数据的函数,与其费力操作DOM,不如确保数据清晰和更新路径正确。
我个人在实际项目中体会最深的一点是:把状态设计放在写代码之前认真思考十分钟,远胜过后面花一个小时处理奇怪的bug。数据应该在哪一层、生命周期如何、哪些状态可以合并、哪些可以省略——这四件事想清楚了,后面的组件设计自然顺畅自然。
最后再分享一个小技巧:当你在调试某个React问题时,先别急着搜代码。打开React Developer Tools的Profiler,录制一段交互,观察哪个组件在频繁渲染、哪个组件的耗时异常突出。很多时候问题在工具面板中一眼就能被定位出来,比你瞎猜要快得多。
另外,如果你正准备深入学习React,我建议把阅读源码当成下半年目标之一。从useState和useEffect这两个入口顺着源码往下读,你会发现很多面试题里的“背概念”,其实就是源码中非常直白的设计决策。那个过程虽然烧脑,但它会把你从“会用”推到“理解”的层面,这是写业务代码很难获得的东西。