ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

在 React/Next.js 中杜绝数组原地排序:用 toSorted() 替代 sort() 保证不可变性(cal.diy 项目实践)

在 React/Next.js 中杜绝数组原地排序:用 toSorted() 替代 sort() 保证不可变性(cal.diy 项目实践) 在 React/Next.js 中杜绝数组原地排序用 toSorted() 替代 sort() 保证不可变性cal.diy 项目实践【免费下载链接】cal.diyScheduling infrastructure for absolutely everyone.项目地址: https://gitcode.com/GitHub_Trending/ca/cal.diy导读在 React 与 Next.js 应用中数组排序是高频操作而Array.prototype.sort()会原地修改数组当被排序的数组恰好来自组件的 state 或 props 时就会破坏 React 的不可变数据模型引发渲染不一致、旧闭包stale closure等难以排查的 Bug。本文以 cal.diy 仓库携带的 js-tosorted-immutable 规则为纲结合项目内真实代码讲清为什么推荐使用 ES2023 的.toSorted()以及在旧运行环境下如何写出同样安全、可落地的排序代码。一、规则出处作为 React/Next.js 编码规范中的一等公民这条规则来源于仓库中随附的 Vercel React Best Practices 技能包。该技能包含 45 条规则、8 大类按影响程度分级其中第 7 大类 JavaScript Performancejs-前缀下的第 7.12 条正是 Use toSorted() Instead of sort() for Immutability与单文件规则 js-tosorted-immutable.md 一一对应。影响等级MEDIUM-HIGH影响说明防止 React state/props 被意外修改prevents mutation bugs in React state适用场景编写新组件、评审代码、重构 React/Next.js 代码时凡涉及对数组排序后再渲染的逻辑都应套用此规则这类规则文件由 frontmattertitle、impact、impactDescription、tags和正文组成设计初衷是让 AI Agent 与 LLM 在维护、生成或重构代码时可直接引用这也解释了为什么正文刻意采用“错误写法 → 正确写法”的对照格式。二、问题根源sort() 的原地修改语义JavaScript 的Array.prototype.sort()从 ES5 起就是原地in-place修改它不返回新数组而是直接重排调用它的数组本身返回值只是对同一数组的引用。const original [3, 1, 2]; const result original.sort((a, b) a - b); console.log(result original); // true返回的是同一个引用 console.log(original); // [1, 2, 3]原数组已被修改这种语义在普通工具函数里问题不大一旦数组来自 React 的 state 或 props就会踩中两条红线。1. 破坏 React 的不可变数据模型React 依赖“props 与 state 只读”的约定来推导 UI 何时需要更新。当你在组件内部用.sort()修改了 props 传入的数组父组件持有的数据悄悄变化但 React 无法感知这次变更引用没变于是当前组件渲染结果可能基于被改乱的数据父组件/兄弟组件引用的同一个数组也已变质出现跨组件“幽灵副作用”useMemo、React.memo、浅比较等基于引用相等性的优化全部失效。规则文件原文如此概括React expects props and state to be treated as read-onlyReact 期望 props 与 state 被当作只读对待。2. 制造旧闭包stale closureBug在回调、effect、事件处理器等闭包中直接sort一个共享数组会让闭包捕获到的数据与外界实际数据相互污染。某个 effect 为了渲染而排了序却可能影响另一个完全不相关的回调读取同一数组时看到的内容这类 Bug 在大型应用中极难定位。三、仓库中的真实“反面教材”在 cal.diy 代码库中确实存在几处对传入数组很可能来自 props 或共享状态直接调用.sort()的写法正是本规则希望避免的形态。示例 1Slider 直接排序 props 数组PopularAppsSlider.tsx 接收items作为组件入参却在渲染属性中直接items.sort(...)export const PopularAppsSlider T extends App({ items }: { items: T[] }) { return ( SliderT title{t(most_popular)} items{items.sort((a, b) (b.installCount || 0) - (a.installCount || 0))} ... / ); };RecentAppsSlider.tsx 是同构的隐患它在 JSX 中直接对items调用了items.sort(...)按创建时间倒序。若这两个组件被多个父组件以同一份数组例如同一个 apps 列表渲染第一次渲染就会把父组件的数组永久重排后续所有基于该数组的其他视图、hooks、缓存都会拿到被打乱顺序的数据。示例 2展示组件内排序 attendees props在 BookingListItem.tsx 的DisplayAttendees子组件中参数attendees直接来自 props 解构却就地排序const DisplayAttendees ({ attendees, ... }: { attendees: BookingAttendee[]; ... }) { attendees.sort((a, b) a.id - b.id); return (...); };这段代码位于每次渲染都会执行的函数体内attendees是从父级传入的 booking 数据任何复用该数组的地方父组件对 attendees 的展开、tooltip、其他渲染分支都会被本次排序波及。仓库中相对安全的对照写法作为对比BookingAttendeesRemoveService.ts 已经采用了规则文件推荐“旧环境降级写法”——先展开拷贝再排序const sortedAttendees [...booking.attendees].sort((a, b) a.id - b.id);同样地LargeCalendar.tsx 在useMemo内先.map()产出全新数组再.sort()因为sort作用的对象是刚创建的新数组所以不会污染bookings原始状态——但这正说明只要语义是“产出一个排好序的新数组”直接使用.toSorted()就是更清晰、更不易出错的选择。四、正确姿势使用 toSorted() 返回全新数组.toSorted()是 ES2023ES14新增的不可变数组方法之一它不修改原数组而是返回一个排好序的新数组比较器签名与.sort()完全一致迁移成本近乎为零。错误写法会修改传入的 users 数组function UserList({ users }: { users: User[] }) { // 直接改写了 users prop 数组 const sorted useMemo( () users.sort((a, b) a.name.localeCompare(b.name)), [users] ) return div{sorted.map(renderUser)}/div }正确写法创建新数组原数组不变function UserList({ users }: { users: User[] }) { // 生成新的已排序数组原数组保持不变 const sorted useMemo( () users.toSorted((a, b) a.name.localeCompare(b.name)), [users] ) return div{sorted.map(renderUser)}/div }规则文件强调的要点新写法不仅避免了 props/state 突变还让useMemo的依赖语义更纯粹——sorted的引用稳定与否只取决于users是否变化与“是否有人提前改过 users”再无瓜葛。五、兼容性与旧环境降级方案.toSorted()已获得广泛支持Chrome 110、Safari 16、Firefox 115、Node.js 20。对更老的环境规则文件给出的降级写法与仓库 BookingAttendeesRemoveService.ts 的实际做法一致——先展开拷贝成新数组再排序// 兼容旧浏览器的降级写法 const sorted [...items].sort((a, b) a.value - b.value)TypeScript 侧的类型支持cal.diy 仓库在 packages/tsconfig/nextjs.json 与 packages/tsconfig/react-library.json 中均将lib配置为[dom, dom.iterable, esnext]且根目录 package.json 声明的 TypeScript 为 5.9.3。esnextlib 已包含 ES2023 的.toSorted、.toReversed、.toSpliced、.with类型签名因此在本仓库类型环境中直接书写这些方法无需额外 polyfill 类型声明但若某处 tsconfig 将lib收窄到es2022以下编译期会报方法不存在届时应使用展开降级写法。六、ES2023 不可变数组方法全家桶规则文件指出.toSorted()并非孤例ES2023 引入了一整组“复制式”不可变数组方法行为上都是不改原数组、返回新数组可统一替换它们对应的就地修改版本不可变方法替代的就地方法作用.toSorted().sort()不可变排序.toReversed().reverse()不可变反转.toSpliced().splice()不可变增删元素.with()arr[index] v不可变替换指定下标元素其中.toSpliced()尤其值得注意.splice()同时做“改原数组 返回被删片段”两件事是 React 代码里最隐蔽的突变源之一改用.toSpliced()后语义与返回值都更符合不可变数据流的习惯。.with(index, value)则填补了“不修改原数组地替换某个元素”的空白——过去必须[...arr.slice(0, i), newVal, ...arr.slice(i 1)]现在一行即可。七、落地清单什么场景必须用 toSorted综合规则文件与仓库实践可归纳出如下判断准则数组来自 props 或 state如 BookingListItem.tsx 的attendees、Slider 组件的items——必须用.toSorted()排序结果要交给useMemo/ 派生状态 / 子组件消费——优先.toSorted()保证原数组引用与内容都不可变排序发生在回调、effect、事件处理器中且数组可能被别处共享——用.toSorted()避免旧闭包问题数组是函数内部新建的局部数组如.map()的产物、[]字面量就地.sort()不会外泄副作用可以安全使用可读性上仍可用.toSorted()统一风格需要兼容 Chrome 110 等旧运行环境——采用[...items].sort(...)降级模式语义与.toSorted()等价。一句话收束React/Next.js 组件里凡是“把数组排个序再去渲染”一律以.toSorted()作为默认选项它把“不可变”从口头约定变成语言级保证让 props/state 永远保持只读也让useMemo、memo 化子组件和并发渲染不再被隐式排序悄悄破坏。若运行环境受限则统一使用展开降级写法切勿把.sort()直接用在共享的 state/props 数组上。【免费下载链接】cal.diyScheduling infrastructure for absolutely everyone.项目地址: https://gitcode.com/GitHub_Trending/ca/cal.diy创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表