React进阶实战:Hooks、实时数据流与高性能图表避坑指南
作为一个写了三年 React 的老开发中间踩过无数次重复的坑也带过好几个新同学上手我决定把这套学习文档一直写到第六期。前面几期把 React 的基础组件、JSX、数据流、路由、调试工具这些讲了但真正到业务里写复杂页面时你会发现光会那点基础根本不够用。这一期我打算集中聊聊几个进阶但绕不过去的话题Hooks 的深层用法、实时数据流怎么接、图表库怎么选、工程化标准怎么定还有大家面试时最高频被问的那些问题。看完这一篇你再回去看自己的项目代码应该会有一种原来这里可以这样写的感觉。文章会包含不少我实际项目里的代码片段和踩坑记录读的时候不用急着看完遇到和你在做的功能能对上的部分停下来打开编辑器照着敲一遍收获会大很多。1. React 状态管理与 Hooks从基础用法到自定义 Hook1.1 状态管理的演进与选型React 的状态管理这几年的演进路线本质上就是状态到底该放哪这个问题的答案变化。最早大家习惯用 Redux因为组件树太深、跨页面共享状态太多不用全局状态管理根本 hold 不住。后来 React 官方出了 Context解决了跨层级传递的问题但它有个天然缺点只要 value 变了所有消费它的组件全都会重渲染性能上很容易翻车。再后来 Hooks 在 React 16.8 里正式落地useReducer Context 的组合开始成为很多中大型项目的默认方案因为它足够原生化没有额外的包体开销也没有 reducer 不能写异步、不能放副作用之类的限制。我现在的选型策略比较固定也推荐你按这个思路来业务复杂度推荐方案理由纯组件内部状态useState最简单直接没有任何心智负担跨多层级传递但更新不频繁Context useContext原生 API够用就好多个组件共享同一份业务状态useReducer Context逻辑集中可预测性强方便测试服务端缓存、异步数据TanStack Query / SWR缓存、去重、重试、更新全帮你管了极端复杂的前端诉求Zustand / Redux Toolkit有中间件生态支持持久化和状态回溯大型团队强约束Redux Toolkit规范最严配合 TypeScript 智能提示完全不用一上来就上 Redux很多项目几百个组件实际共享的状态不超过三份。为了三份状态引入 Redux 全家桶reducer、action、selector 各种模板代码堆一屏反而拖慢开发速度。1.2 自定义 Hook 的设计原则与复用自定义 Hook 是我认为 React 后期最值得认真研究的一个点。它能让你把状态逻辑从UI 逻辑里彻底拆出来。一个干净的业务组件应该是JSX 负责长什么样自定义 Hook 负责状态从哪来、怎么变。我自己常用的一个自定义 Hook作用是管理弹窗的开合和表单数据大家感受下这个写法function useModalWithInitialData(initialData) { const [visible, setVisible] useState(false); const [formData, setFormData] useState(initialData); const open useCallback((data) { setFormData(data || initialData); setVisible(true); }, [initialData]); const close useCallback(() { setVisible(false); setFormData(initialData); }, [initialData]); return { visible, formData, open, close, setFormData }; }有人说 Hook 就要短小精悍但这个 Hook 不是为了拆而拆它把打开前回填数据、关闭后清空状态这个高频业务逻辑固化下来。多个弹窗组件直接用五个返回值永远不用担心某个弹窗关闭了还留着上一次的数据。设计自定义 Hook 时我总结出三条心法给你参考。第一条返回的是一个行为集合不是一个单一值。我见过有人写 useFetchData返回一个 data 就完事了但实际业务里你必然还需要 loading、error、refetch这些是一个整体应该一起返回。第二条如果 Hook 里同时用到了 useState 和 useEffect想一想这个 Hook 是不是在做同步外部变化到内部状态的事。很多时候这类逻辑应该交给数据请求库而不是自己手写 useEffect 去同步容易出竞态问题。第三条命名非常重要。use- 开头是铁的规矩但你给 Hook 起名字时要表达它赋予组件什么能力而不是它内部调用什么 API。useLocalStorage 比 useSyncStorage 好usePagination 比 usePageChange 好。名字是给后来人读的多花十秒钟想清楚后面省十分钟。1.3 useState 与 useReducer 的分界线很多初学者搞不清什么时候用 useState什么时候用 useReducer。我的判断标准很简单当状态的更新逻辑里出现了根据一个状态计算另一个状态的连锁或者一次交互要更新两个以上字段时就该上 useReducer。举个例子一个筛选面板包含关键词、分页页码、排序方式、筛选标签四个字段。如果用 useState每个筛选条件的变化都要写一段 setState相互之间还可能有联动比如切换筛选标签要重置页码。这种逻辑写四个 useEffect 监听各自状态再相互设置极易产生多余的渲染循环。用 useReducer把所有的筛选状态归拢到一个 reducer 里一个 action 就能完成联动更新const [filter, dispatch] useReducer( (state, action) { switch (action.type) { case SET_KEYWORD: return { ...state, keyword: action.payload, page: 1 }; case SET_TAG: return { ...state, tag: action.payload, page: 1 }; case SET_PAGE: return { ...state, page: action.payload }; default: return state; } }, { keyword: , tag: null, page: 1, sort: latest } );这种写法好在哪它把联动规则集中在 reducer 里。换关键词必须回到第一页这个业务规则变成了一个显式的行为不管你是从哪里触发的都会遵守。这比散落在各个组件里的 if-else 判断要可靠得多。提示useReducer 的 reducer 必须保持纯净。不要在 reducer 里发请求、不要读 Date.now()、不要改 DOM。如果必须做这些副作用放到 action creator 或者 useEffect 里否则 StrictMode 下会出诡异问题。2. 实时数据流实践SSE 与 WebSocket 在 React 中的正确用法2.1 SSE 与 WebSocket 怎么选服务端推送数据到前端主流方案就是 SSE 和 WebSocket。好多人一上来就选 WebSocket理由就是它是全双工听着高大上。但实际业务里相当一部分场景只需要服务端往客户端推客户端并不需要实时往服务端发数据比如通知推送、日志流、任务进度、文件变化监听。这种场景用 SSE 更合适。SSE 是基于 HTTP 的一大优势是你不需要单独维护一套 WebSocket 服务和握手协议。前端用一个 EventSource 实例就能收消息断线了浏览器会自动重连这个自动重连帮我们省掉了大量的自研逻辑。需要双向通信的场景比如聊天室、实时协作编辑器、多人在线游戏才需要 WebSocket。判断标准就一条你的客户端需要不在用户主动操作的情况下实时地向服务端发消息吗需要就上 WebSocket不需要就乖乖用 SSE。2.2 React 中接入 SSE 与 WebSocket 的完整示例先看 SSE。我在一个日志监控项目里用 EventSource 接后端日志流代码长这样function useSseLogStream({ url, onEvent }) { const [logs, setLogs] useState([]); const [connected, setConnected] useState(false); useEffect(() { const es new EventSource(url); es.onopen () setConnected(true); es.onerror () { // EventSource 自动重连这里只负责更新状态 setConnected(false); }; es.onmessage (e) { const parsed JSON.parse(e.data); setLogs((prev) [...prev.slice(-199), parsed]); // 只保留最近200条 onEvent?.(parsed); }; return () es.close(); // 组件卸载记得关闭否则连接会一直挂着 }, [url]); return { logs, connected }; }注意这个写法有个关键点setLogs用的函数式更新这样每次都是基于最新状态追加数据不会出现闭包里的旧logs覆盖新数据的问题。再看 WebSocket。WebSocket 的坑比 SSE 多因为它不自动重连而且收到消息后的事件处理容易写成一坨。我建议把连接、重连、心跳、消息分发全部封装进一个自定义 Hook 里function useWebSocket(url, handlers) { const wsRef useRef(null); const heartbeatRef useRef(null); const handlersRef useRef(handlers); handlersRef.current handlers; useEffect(() { let disposed false; let retryCount 0; const connect () { const ws new WebSocket(url); wsRef.current ws; ws.onopen () { retryCount 0; heartbeatRef.current setInterval(() { if (ws.readyState WebSocket.OPEN) ws.send(__ping__); }, 30000); }; ws.onmessage (e) { const msg JSON.parse(e.data); handlersRef.current?.[msg.type]?.(msg.payload); }; ws.onclose () { clearInterval(heartbeatRef.current); if (!disposed retryCount 5) { retryCount 1; setTimeout(connect, 1000 * retryCount); // 指数退避1s 2s 3s 4s 5s } }; ws.onerror () ws.close(); }; connect(); return () { disposed true; clearInterval(heartbeatRef.current); wsRef.current?.close(); }; }, [url]); const send (type, payload) { if (wsRef.current?.readyState WebSocket.OPEN) { wsRef.current.send(JSON.stringify({ type, payload })); } }; return { send }; }这段代码里值得留意的细节有三个第一handlersRef的写法是为了让消息回调永远拿到最新的 handler同时又不干扰 useEffect 的依赖第二重连用了指数退避避免后端一抖动所有客户端同时疯狂重连直接把服务打挂第三30 秒一次心跳这个时间可以按业务调整如果心跳间隔内没收到任何消息就可以判死重连了。2.3 文件变化轮询与实时更新从热词react sse/websocket 轮询文件变化来看很多人在做文件监听、构建日志、CI 进程状态这类功能。这里有个常见误区以为监听文件变化就一定要用 WebSocket。实际上如果你的场景是本地编辑器里监控构建进程输出的日志前端只需要被动接收那用 SSE 经验上更省心。我给一个文件监听场景的实操思路后端用 chokidar 监听目录文件变更后把事件add / change / unlink / changeDetail推到消息队列前端通过 SSE 订阅。React 端拿到事件后维护一个文件状态 Mapconst [fileMap, setFileMap] useState(() new Map()); useSseLogStream({ url: /api/file-events, onEvent: (evt) { setFileMap((prev) { const next new Map(prev); if (evt.type unlink) { next.delete(evt.path); } else { next.set(evt.path, evt); } return next; }); }, });用 Map 而不是数组是因为文件路径具有唯一性用 Map 天然去重更新时也不需要遍历查找。这条经验是我在真实项目里踩出来的第一次我用数组实现两千个文件的时候每次更新要find一遍页面卡到没法看。注意WebSocket 和 SSE 的 URL 在大部分开发环境下需要走代理。如果你配了 WebpackDevServer 或 Vite记得在代理配置里把/api这种前缀的请求转发到后端服务否则前端永远连不上。3. 数据可视化集成从通用图表到 K 线图3.1 React 图表库选型对比React 里做图表市面上主流的方案就那几个ECharts、Ant Design Charts、Recharts、Chart.js还有一个很多人不熟但性能极佳的 uPlot。我自己的选型经验是先看你要什么图表类型再看性能要求最后才看 API 风格。库定位图表类型性能适合场景ECharts全能型折线、柱状、饼图、地图、K 线什么都有中规中矩大数据量需配合 samplint后台管理系统、数据大屏Recharts轻量易用常见统计图一般简单报表、内部工具Ant Design Charts基于 G2偏统计分析中上Ant Design 技术栈团队uPlot极致性能折线图、K 线图极快支持百万点渲染高频实时数据、金融行情图ECharts 和 uPlot 是一个典型对比ECharts 功能全、文档丰富、配置项五花八门但它底层是 canvas 封装的渲染方案数据量超过几万条时交互会明显变肉。uPlot 的设计目标就是快它自己说 1 毫秒内能渲染 1 万个点实际用下来在金融行情这种高频场景里确实比 ECharts 顺滑一个量级。3.2 uPlot 集成 K 线图的踩坑实录用 uPlot 画 K 线图是我觉得 React 图表这块最有价值的案例之一。K 线图的要求是每个 K 线包含开、高、低、收四个价格还要在鼠标悬浮时看到十字线提示和 OHLC 数值。uPlot 本身没有内置 K 线图你得用它的自定义 series 回调去画。我的做法是用series的draw回调结合 canvas 原生 API 手绘每个 K 线的蜡烛和影线。核心代码片段const series [ { label: K, scale: x, // 自定义绘制 K 线 draw: (u, serieIdx) { const ctx u.ctx; const { x, y } u; const data u.data[0]; ctx.save(); data.forEach((item, i) { if (item null) return; const [time, open, close, high, low] item; const xPos x.toGrid(i); const yOpen y.toGrid(open); const yClose y.toGrid(close); const yHigh y.toGrid(high); const yLow y.toGrid(low); // 画影线 ctx.strokeStyle close open ? #ef5350 : #26a69a; ctx.lineWidth 1; ctx.beginPath(); ctx.moveTo(xPos, yHigh); ctx.lineTo(xPos, yLow); ctx.stroke(); // 画蜡烛 const top Math.min(yOpen, yClose); const height Math.max(1, Math.abs(yOpen - yClose)); ctx.fillStyle close open ? #ef5350 : #26a69a; ctx.fillRect(xPos - 4, top, 8, height); }); ctx.restore(); }, }, ];这个写法有两个细节容易被坑到。一个是 uPlot 的坐标转换u.data[0]里的原始值是秒级时间戳和价格必须用x.toGrid(i)和y.toGrid(value)才会被转成 canvas 坐标系里的像素位置。直接拿原始数值画图画出来的 K 线位置铁定是错的。另一个是蜡烛宽度我固定用 8 像素这在 K 线图缩放时会显得别扭更好的做法是按 x 轴的实际跨度动态计算宽度比如(u.plot.width / data.length) * 0.8这样在不同时间粒度下K 线的疏密关系才自然。uPlot 不提供悬浮提示你在 React 里得自己监听uplot实例的mouseenter、mousemove事件再根据u.cursor.left反算出对应的数据索引。我封装了一个 useKlineCursor 的 Hook逻辑大概是记录当前 cursor 的 x 像素值用u.posToVal(left, x)转成时间戳再二分查找最近的数据项。数据量不大时用findIndex也行但 K 线数据往往上万条二分查找性能差距很明显。3.3 图表大数据量渲染的三个优化点图表性能优化这个话题面试常问实际项目也躲不过。拿 K 线图举例一天逐笔数据可能就有 10 万条直接全量传给图表库无论哪个库都会卡。我的优化三板斧是这样的。第一板斧是后端聚合下钻。先展示日 K用户缩小时间范围后再请求更细粒度的分钟数据。这是最有效的从源头减少数据量大量线上行情软件都是这么干的。在 React 里只需要在时间范围变化的 useEffect 里去请求配合 debounce 控制请求频率。第二板斧是前端降采样。如果后端没法聚合那就前端做抽稀。比如用 LTTB 算法把 10 万条抽成 5000 条抽稀后的折线形状和真实数据几乎一致。这块可以引入lttb这个小库写起来很省事不用自己实现实测肉眼看不出区别。第三板斧是避免不必要的 React 重渲染。图表更新时如果整张图表都用 state 驱动每次数据更新整个图标组件树都重渲染纯属浪费。我习惯把图表实例放进 ref数据更新时直接调用图表实例的setData方法完全绕过 React 的渲染机制。这听起来像反模式但对于以 canvas 渲染为核心的图表库React 的重渲染对 canvas 没有任何帮助canvas 内部自己会重绘你何必让 React 再跑一遍 diff提示千万别在图表组件里用 React 的 state 去保存当前悬浮的数据索引这类高频变化的值。鼠标移动一秒触发几十次 setState组件会疯狂重渲染图表会明显掉帧。把这类高频临时状态存在 useRef 里需要展示到 UI 上时再局部更新需要变化的那一小块 DOM。4. 工程化规范与避坑实战4.1 通用 React 开发标准的落地从热词有没有通用 react 开发标准能看出来很多团队都在纠结这件事。我给自己的团队定了一套标准不算复杂但确实能挡住绝大部分低级问题给你参考。组件命名一律帕斯卡命名文件名和组件名必须一致。函数和变量用驼峰。CSS 变量用短横线。目录上我采用src/components、src/hooks、src/utils、src/types、src/pages五层结构。components 里每个组件一个目录内部放index.tsx、types.ts、style.module.css超过两个子组件就拆成子目录。这套目录看久了会发现就算项目换三个人维护找文件也不用想路径永远在那几个位置。组件规范这里最大的雷区是不要在组件里直接写超过 30 行的 useEffect。如果一个 useEffect 里又是请求、又是 setState、又是订阅那它的副作用太复杂了你要么把它拆进自定义 Hook要么拆成多个 useEffect每个只负责一件事。我后来的代码评审checklist 大概就这七项有没有命名不达意的 Hook 或组件、useEffect 有没有超过一个职责、有没有直接在 JSX 里写复杂三元表达式、有没有重复的 setState 逻辑可以合并成 useReducer、api 层是否统一封装了错误处理、有没有忘记清理定时器或订阅、关键业务逻辑有没有单测。这套东西不是文档是 review 的时候逐条过的清单。约定只有形成评审时会被质疑的强制力才会有真正的约束作用。4.2 React Native 启动白屏排查热词里出现react native 启动白屏这个我太熟了。RN 启动白屏十个里有八个不是同一个原因但排查路径是有规律可循的。第一步先区分是 Debug 模式还是 Release 模式。Debug 模式白屏几乎都是 Metro 打包服务的问题没启动 Metro、端口占用、代码路径错误、bundle 加载超时。终端里跑npx react-native start --reset-cache清了缓存启动再杀掉所有 node 进程能解决一半问题。Release 模式白屏重点看原生资源和 JS bundle 的版本是否匹配改了MainActivity或MainApplication后没有重新编译原生工程、Android 的 proguard 混淆规则把某些类混淆没了、iOS 的 release scheme 跑的还是旧 bundle。第二种常见原因是首屏渲染太重。RN 加载完 JS bundle 后如果你在第一个页面里同步执行了大量计算、或者 loaded 了超大的首屏数据、甚至直接启动就连接 WebSocket 同步数据都会导致首帧迟迟渲染不出来。这种白屏的优化思路是首屏组件只渲染必要的占位结构重任务全部放到InteractionManager.runAfterInteractions或者requestIdleCallback里去执行。首屏数据走后端接口异步加载loading 骨架屏给用户一个反馈体验会好非常多。还有一类白屏是因为 Splash 页面跳转逻辑设计不合理。Splash 页面持有路由跳转跳转条件依赖某个异步初始化任务比如 SDK 初始化、登录态检查这个任务卡住了跳转永远不执行页面就一直停留在 Splash 阶段。这个问题排查时最容易被忽视的是初始化任务用了全局的 static 变量第一次启动没事第二次启动时 static 变量残留导致条件判断直接走错分支。这时候把启动流程打日志从componentDidMount到跳转的每一个分支都打上标记一眼就能定位。4.3 常见的 React 性能问题与排查思路React 性能问题我见过最多的是以下四种每个都有对应的排查手段。无效重渲染排第一。父组件每次 setState子组件全部跟着重渲染。解决办法是给子组件包memo并且让传入的 props 保持稳定。这里有个隐藏陷阱很多人只给子组件加 memo但父组件往子组件传了一个内联定义的箭头函数onClick{() handle(1)}这个函数每次父组件渲染都是新引用memo 直接失效。正确写法是用useCallback把函数缓存起来依赖项变的时候才生成新函数。第二是大列表渲染。列表超过 1000 项或者列表项内部组件较复杂直接 map 渲染绝对卡。方案是虚拟列表用react-window或react-virtualized。我第一次用react-window时把 3 万条列表的卡顿直接降到了只剩滚动流畅体感从 10 分卡降到 1 分不卡。第三是图片和动画卡顿。图片要懒加载用loadinglazy或 IntersectionObserver。CSS 动画优先用 transform 和 opacity避免在动画里改 top、left、width 这些会触发布局回流的属性。能用will-change提前声明动画属性也能避免一些跳帧。第四是 bundle 体积过大。首屏加载的 JS chunk 太大网络慢的地区白屏很久。查包 m 大小时下载source-map-explorer构建后打开可视化分析一屏就能看出是哪个依赖占了大头。常见的优化是路由懒加载、第三方库按需引入、把 antd、echarts 这类大库拆成独立 chunk 单独缓存。5. 面试、AI 与 React 的下一步5.1 高频 React 面试题与关键思路围绕react 面试题react 面经这两个热词我把这些年面试候选人和自己被人问过的问题做了个筛选去掉那些过于简单的比如生命周期函数是哪几个留下几个确实能拉开层次的核心题顺带把参考答案的思路也讲了。第一个必问的是useEffect 的依赖数组是什么意思闭包陷阱是怎么产生的这个问题能看出你平时写代码时有没有真的理解为触发时机。完整答法要说清楚依赖数组里只要有引用类型的值就必须保证这个引用是稳定的否则每次渲染都会重新执行 effect。闭包陷阱则是 effect 内部捕获了旧渲染的 props 或 state解决方案是函数式更新、useRef或者把依赖写全。第二个高概率题是React 的 key 到底选的什么很多人答用 id 就行这不够。要答key 是用来帮助 React 识别哪些元素改变了不传 key 或传不稳定的 key比如随机数复用会错乱。列表项顺序可能变化时不能用 index 当 key因为那样 React 会认为元素没变只是内容变了导致组件内部状态错乱。如果你有一个拖拽排序列表或者一个需要记住输入框内容的动态列表用 index 当 key 一定会出 bug。第三个是React 18 的并发特性你用过哪些这个问题是区分做题家和实战者的题。你不用把useTransition、startTransition、Suspense每个都背得滚瓜烂熟但你要能说清楚自己在哪个场景下用过它们。我的经历是在搜索输入框里用useTransition把过滤大列表这个非紧急更新标记为过渡这样用户敲字时 UI 不会卡顿在路由懒加载的落地页用 Suspense 包一层 fallback。第四个是为什么说派生状态要避免在渲染中直接改这是 React 面试里最反直觉的问题。派生状态指的是组件根据 props 计算出的状态你在渲染函数里直接if (props.a ! state.a) setState(...)虽然在 React 里能跑但它是所有渲染中 setState 的经典反面教材因为它会让你的组件在渲染中途触发更新React 那种弹性的渲染流程会被打断。正确做法是用useMemo去算派生值或者在key层面控制组件重置或者componentDidUpdate里比对更新。5.2 React 与其他框架的差异化竞争力热词里ai react 框架和其他框架的区别其实是面试里经常变相问的。Vue 和 React 的对比都快被问烂了但答案其实很稳定Vue 的双向绑定和模板语法上手快、模板内做的优化多React 的 JSX 更贴近纯 JavaScript组合性和类型推导更强生态选择更自由。但如果你问的是未来几年的竞争力我认为 React 的差异化优势集中在这几件事上。一件事是 Hooks 模式已经变成了前端框架的通用语言。React 提出 Hooks 之后Vue 也有 Composition API、Solid 也做类似的设计这套基于函数的心智模型已经成了事实标准。做 React 出身的人去看其他框架学习成本很低因为原理是相通的。另一件事是 Server Components 与 RSC 架构。React 在服务端组件这个方向上的探索是很多框架没有的。它让组件可以在服务端运行、直接抓取数据、然后把结果序列化给客户端从而减少客户端 JS bundle 大小、减少客户端数据请求。这个能力已经大面积落地React 团队明确表示这是 React 未来十年的方向。如果你现在找 React 相关的工作把 RSC 的基本原理和适用场景搞清楚会很有优势。第三件事是 React 社区的生态规模。图表、拖拽、编辑器、可视化、AI 智能体集成几乎所有 JavaScript 生态里出现的新工具都能在 React 社区里找到对应的封装。做项目选型时生态丰富意味着你踩过的坑大概率有人也踩过能少走很多弯路。5.3 从 React 模式到 AI 智能体状态驱动与行动闭环热词基于 react 模式构建能思考与行动的 ai 智能体这个方向特别有意思。你认真想想React 的核心心智模型是什么是状态决定 UIUI 变化触发行为行为又反过来更新状态这个闭环本质上就是一个智能体的思考-行动循环Agent 拿到目标根据当前状态思考下一步动作执行动作动作的结果再更新状态如此往复直到目标完成。现在很多 AI 应用的前端架构比如 AI 对话机器人本质就是一个特殊的状态机。用户的每一条输入是 action模型的回复是状态更新页面里不同的渲染分支是根据 state.status 派生出来的。这也是为什么 React 在这类应用里特别顺手的原因你可以把 Agent 的运行过程建模成一个个 distinct 的 state 节点thinking、waiting_input、executing_tool、error然后让 UI 根据 state 渲染不同的界面而不是像传统命令式写法那样靠回调层层嵌套。我建议感兴趣的同学可以做一个这样的小项目用 useReducer 管理一个AI 智能体任务的状态机reducer 里定义START、GET_RESULT、TOOL_CALL、FINISH、ERROR这些 action。UI 只负责把 state.status 渲染成 loading 动画、结果面板、错误提示。模型返回的 tool call 结果再 dispatch 给 reducer形成闭环。做一遍之后你会对 React 状态管理有一个全新的理解也会对 Agent 应用的前端架构有具象的认知。6. 实测避坑清单与高频问题速查把这一篇涉及到的坑整理成一个速查表方便你在写代码时对照自查。这些都是我在实际项目里遇到过、排查过、解决过的问题可靠程度比单纯读文档高出很多。问题现象根本原因解决方案自定义 Hook 里 setState 后拿不到最新值setState 是异步的本次渲染周期的值还没提交用 useEffect 监听该状态或使用函数式更新useEffect 里的异步请求出现竞态组件卸载或依赖变化前一个请求后返回用一个 disposed 布尔变量在 cleanup 里置为 true使用setInterval后页面一直报错泄漏没有在 cleanup 里 clearIntervaluseEffect 返回清理函数组件卸载时清理图表组件里用 useState 存 cursor 后鼠标卡顿高频 setState 引起整个组件重渲染用 useRef 存 cursor仅通过局部 DOM 更新 UIEventSource 连接一直挂着导致内存泄漏组件卸载前没调用 closeuseEffect cleanup 里调用es.close()WebSocket 断线后不再连接没写重连逻辑封装带指数退避的重连函数列表用了 index 作为 keyindex 不稳定Render 复用错乱用唯一 stable 的 id组件加了 memo 但子组件仍每次重渲染传入子组件的 props 引用不稳定内联函数/对象配合useCallback、useMemo固化 propsReact Native 白屏 Metro 一直报错Metro 缓存损坏--reset-cache启动图表数据量大导致卡顿渲染了过多数据点后端聚合 LTTB 抽稀 只更新局部 DOM这个表我建议直接贴在项目文档里每次 code review 时候翻一翻比临时去想解决方案效率高得多。注意以上的排查思路都是基于作为前端框架使用者的视角而非 React 原生源码分析。如果你想更深入建议自己把 React 源码里scheduler、fiber reconciler这两个模块读一遍读完再回头看这些问题理解会通透得多。我在日常项目里最大的体会是React 其实并不难难的是你脑子里有没有一套状态驱动的思维模型。用这个模型去想问题页面是什么状态我能对状态派发什么 action状态变化后 UI 该变成什么想通了所有的数据流、性能优化、组件设计都变得自然而然。后面如果你们想看我可以再写一期专门讲 React 18 的 concurrent 特性怎么在真实项目里用的案例或者把 RSC 的应用实践写透。到时候评论区喊一声就行。