当前位置: 首页 > news >正文

React 18 useTransition 实战:过滤大列表、切 Tab 时保持输入不卡顿

React 18 useTransition 实战:过滤大列表、切 Tab 时保持输入不卡顿

你做过一个带搜索框的大列表:输入框下面是几千条数据,每敲一个字符就实时过滤。用户打字时明显感觉输入框「粘手」——字母延迟半拍才出现,退格也卡。你检查过没有多余请求、没有死循环,罪魁祸首其实是:每次输入都触发一次几千条的重渲染,而这个重渲染和「更新输入框」抢占同一优先级。React 18 的useTransition就是为这种场景造的,它能告诉 React「这次更新不急,别挡着用户打字」。

问题复现:输入和过滤挤在一起

先看没优化的版本,一个 5000 项列表的实时过滤:

import { useState } from "react"; const ALL = Array.from({ length: 5000 }, (_, i) => `Item ${i}`); function SearchList() { const [query, setQuery] = useState(""); const [list, setList] = useState(ALL); function handleChange(e) { const value = e.target.value; setQuery(value); // 更新输入框 setList(ALL.filter((x) => x.includes(value))); // 过滤 5000 项,重渲染很重 } return ( <> <input value={query} onChange={handleChange} /> <ul> {list.map((x) => <li key={x}>{x}</li>)} </ul> </> ); }

这里两个setState是同一优先级,React 会在同一次渲染里既更新输入框、又渲染 5000 个<li>。列表渲染慢,输入框的更新就被它拖着一起慢,于是打字卡顿。

用 useTransition 把过滤标记为「非紧急」

useTransition返回[isPending, startTransition]。把「重的、可以晚一点」的更新包进startTransition,React 就会优先处理紧急更新(输入框),把 transition 更新放到后面、且可被后续输入打断:

import { useState, useTransition } from "react"; const ALL = Array.from({ length: 5000 }, (_, i) => `Item ${i}`); function SearchList() { const [query, setQuery] = useState(""); const [list, setList] = useState(ALL); const [isPending, startTransition] = useTransition(); function handleChange(e) { const value = e.target.value; setQuery(value); // 紧急更新:输入框必须立刻响应 // 非紧急更新:过滤结果晚一点没关系,还能被下一次输入打断 startTransition(() => { setList(ALL.filter((x) => x.includes(value))); }); } return ( <> <input value={query} onChange={handleChange} /> {isPending && <span>过滤中…</span>} <ul style={{ opacity: isPending ? 0.6 : 1 }}> {list.map((x) => <li key={x}>{x}</li>)} </ul> </> ); }

现在打字始终跟手:setQuery是紧急的,立刻更新;setList被标记为 transition,React 会在空隙里去算,而且如果你在它算完前又敲了一个字符,上一次的过滤会被直接丢弃,不做无用功。isPending让你在过渡期间给个「过滤中」的视觉反馈,避免界面看起来卡住。

关键规则:startTransition 里只能放 set,不能放读到的值

一个高频踩坑:很多人想把输入值本身也放进 transition,结果输入框反而更卡了。记住驱动输入框的那个 state 必须留在 transition 外面:

// ❌ 错误:把 setQuery 也包进去,输入框更新被降级,打字更卡 startTransition(() => { setQuery(value); setList(ALL.filter((x) => x.includes(value))); }); // ✅ 正确:输入框紧急更新在外,重活在里面 setQuery(value); startTransition(() => { setList(ALL.filter((x) => x.includes(value))); });

还有一点:startTransition的回调必须是同步的。如果你在里面写await fetch(...)setState,那个setState已经脱离了 transition 上下文,不会被当作非紧急更新。异步数据场景要用use或数据请求库配合 Suspense,而不是把 await 塞进 startTransition。

切 Tab 场景:同样适用

useTransition不只用于搜索。任何「点一下会触发重渲染,但你希望点击本身立刻有反馈」的场景都合适,典型的是切换 Tab 加载不同的重内容:

function Tabs() { const [tab, setTab] = useState("home"); const [isPending, startTransition] = useTransition(); function selectTab(next) { // 让 Tab 的高亮切换立刻发生,重内容的渲染延后 startTransition(() => setTab(next)); } return ( <> <nav> {["home", "posts", "charts"].map((t) => ( <button key={t} onClick={() => selectTab(t)} style={{ fontWeight: t === tab ? "bold" : "normal" }} > {t} </button> ))} </nav> {isPending && <p>加载中…</p>} <TabContent tab={tab} /> {/* 假设 charts 这个 tab 渲染很重 */} </> ); }

点击charts时,即使它的内容渲染要 200ms,isPending会立刻变true给出反馈,用户不会觉得点击「没反应」,而旧的 Tab 内容还留在屏幕上直到新内容准备好。

useTransition vs useDeferredValue:怎么选

两者都能解决「重更新拖累交互」,区别在于你控制的是「触发」还是「值」:

  • useTransition:你能改动那次setState的代码时用它,直接把 set 包进startTransition。额外送你一个isPending
  • useDeferredValue:你拿到的是别人传进来的 prop / 已有 state,改不了它的 set 调用时用它,const deferred = useDeferredValue(value),让派生渲染用延迟值。

一句话:能碰 setState 就用useTransition,只能碰值就用useDeferredValue

小结

  • 卡顿的根因常是「紧急更新(输入/点击)」和「重更新(大列表渲染)」挤在同一优先级。
  • useTransition把重更新包进startTransition,让 React 优先响应交互,过渡更新可被后续操作打断,不做无用功。
  • 驱动输入框的setState必须留在 transition外面,否则越优化越卡;startTransition回调要同步,别塞await
  • isPending给过渡期一个视觉反馈,界面不会看起来「死了」。
  • 一句话记忆:急的放外面,重的进startTransition;改得了 set 用useTransition,只拿得到值用useDeferredValue
http://www.jsqmd.com/news/1309596/

相关文章:

  • GD32H75E DAC深度解析:从寄存器配置到低功耗波形生成实战
  • VMware虚拟机共享文件夹配置与优化指南
  • 二分法算法(水平等分图形面积)
  • 跨平台流媒体下载利器:N_m3u8DL-RE如何破解DASH/HLS/MSS下载难题
  • 英国“巨像”计算机的绝密二战史
  • Unity音频可视化神器LaspVfx:让你的游戏特效随音乐律动
  • Google Workspace CLI高级教程:Drive文件批量操作与共享管理
  • PHP变量存储与系统生命周期深度解析
  • 2026年重庆节能极窄门窗定制服务商采购参考指南:极窄门窗、智能极简门窗、高端大宅门窗定制 - 海棠依旧大
  • ScanTailor Advanced终极指南:免费开源文档扫描优化神器
  • Box64终极指南:5个技巧让ARM设备高效运行x86程序
  • 2026年鱼池防漏水选C35P8整体现浇还是砖砌?结构强度差距惊人
  • ClawSec自动化工作流解析:从NVD数据拉取到社区顾问发布的全流程
  • 如何用普通WiFi信号实现无摄像头人体姿态追踪:RuView隐私保护监测完整指南
  • AI越来越聪明,造它的人却越来越害怕
  • 华硕笔记本性能控制新选择:G-Helper如何重塑你的使用体验
  • 艺绘空间书法艺术专业吗
  • 【独家首发】信通院认证的AI云边协同成熟度评估矩阵(含18项量化指标+自测工具包)
  • 终极指南:如何用CausalNex从数据中发现因果关系
  • ESP32-S3驱动1.85寸触摸屏:从硬件选型到LVGUI实战开发
  • 解锁企业数据采集能力:gh_mirrors/co/company-crawler核心功能详解
  • 2026年渝苏极简极窄门窗品牌选择指南:行业观察与主体评测分析 - 海棠依旧大
  • 艾德克斯与施耐德电气携手,硬核测试设备护航第五届高校电气电子工程创新大赛全国总决赛
  • Windows 10字体渲染终极优化指南:BetterClearTypeTuner实战应用
  • 小熊猫Dev-C++:为什么这个轻量级C++开发环境是你的最佳选择?
  • ClawSec部署最佳实践:生产环境中的安全强化与性能优化
  • 哪个GPU租用品牌更适合图像渲染?用一段30分钟的小样找答案
  • Lance湖仓格式:如何用2行代码实现100倍性能提升的AI数据管理方案
  • 3步掌握WanVideo_comfy:ComfyUI视频生成终极资源库
  • 【Docker】7.Docker安装