Cal.com 前端实践:用 toSorted() 替代 sort() 避免 React 状态原地变异 bug 📅 发布时间:2026/9/8 21:05:03 👁 浏览次数: Cal.com 前端实践用 toSorted() 替代 sort() 避免 React 状态原地变异 bug【免费下载链接】cal.diyScheduling infrastructure for absolutely everyone.项目地址: https://gitcode.com/GitHub_Trending/ca/cal.diy本文基于 Cal.com 仓库内置的 Vercel React 最佳实践规则集js-tosorted-immutable规则展开.sort()会原地修改数组在 React 的 props/state 场景下会破坏不可变模型并引发陈旧闭包 bug应改用返回新数组的.toSorted()。读完你将掌握两种写法的差异、在 Cal.com 真实代码中的正确/错误实例以及老环境的降级方案和同类不可变方法的完整清单。规则定位js-tosorted-immutable 在规则集中的位置该规则文件为 js-tosorted-immutable.md属于 vercel-react-best-practices 技能 的一部分。根据 SKILL.md 的优先级表这套技能共包含 45 条规则、8 个类别js-前缀的JavaScript Performance类别影响等级为 LOW-MEDIUM而本规则自身标注的影响级别为MEDIUM-HIGH影响描述为 prevents mutation bugs in React state——即在规则类别中它属于优先级不高但实际危害不小的一条单行 API 替换即可消除一类难以排查的状态污染问题。规则核心结论只有一句话.sort()mutates the array in place, which can cause bugs with React state and props. Use.toSorted()to create a new sorted array without mutation.错误写法对 props 直接调用 sort() 会污染源数据原始规则文档给出的反例是一个典型的用户列表组件function UserList({ users }: { users: User[] }) { // Mutates the users prop array! const sorted useMemo( () users.sort((a, b) a.name.localeCompare(b.name)), [users] ) return div{sorted.map(renderUser)}/div }这段代码有两个隐蔽问题users.sort()返回的就是users数组本身原地排序sorted与users指向同一引用。父组件传入的 props 数组被就地改写React 的 props/state 不可变模型被破坏由于useMemo依赖[users]而排序并没有产生新引用React 无法感知数据变化后续渲染可能拿到已被污染的顺序——这正是规则文档列出的两类后果之一陈旧闭包/意外行为。正确写法用 toSorted() 生成新数组function UserList({ users }: { users: User[] }) { // Creates new sorted array, original unchanged const sorted useMemo( () users.toSorted((a, b) a.name.localeCompare(b.name)), [users] ) return div{sorted.map(renderUser)}/div }.toSorted()的语义是复制一份再排序原数组保持不变sorted是新数组useMemo依赖变化时能正常产出新引用下游渲染与 memo 比较都能按预期工作。为什么这在 React 中特别重要规则文档给出了两条核心理由结合 Cal.com 这类以 tRPC React 组件化组织的前端架构可以进一步展开props/state 变异破坏 React 不可变模型。React 假设 props 与 state 是只读的。变异发生在子组件内部时父组件的 state 数组悄悄改变但引用没变——React.memo、useMemo、useEffect依赖比较全部失效bug 表现是排序结果错乱但控制台没有任何报错这类问题在多组件共享同一数据源时例如列表页与详情抽屉同时消费同一数组尤为难查。闭包内的变异导致陈旧闭包 bug。在回调、effect 中排序一个来自闭包捕获的数组会让多个渲染周期共享同一份被改写的存储产生跨渲染的意外状态。仓库内的真实实例原地排序 vs 先展开再排序当前仓库基于代码检索尚未使用.toSorted()但在 Web 应用中能找到规则文档所述两类模式的真实样本正好构成一组对照。原地变异 props 的样本RecentAppsSlider.tsx 直接在渲染期对传入的itemsprop 排序export const RecentAppsSlider T extends App({ items }: { items: T[] }) { const { t } useLocale(); return ( SliderT title{t(recently_added)} items{items.sort( (a, b) new Date(b?.createdAt || 0).valueOf() - new Date(a?.createdAt || 0).valueOf() )} ...同目录下的 PopularAppsSlider.tsx 也是同样的items.sort(...)写法。按照规则文档的标准这两处都会在每次渲染时改写父组件持有的数组。从源码结构看这类滑块组件的items直接来自父级传入的 prop若父组件后续还基于原顺序渲染其他内容就会读到已被排序改写的顺序——这是规则要防的典型场景仓库现状如此是否重构由维护者决定此处仅作为规则适用的实例呈现。先展开再排序的合规样本Embed.tsx 在嵌入组件的时长摘要渲染中处理selectedDateAndTime时采用了文档推荐的降级写法——先复制再排序{selectedDateAndTime Object.keys(selectedDateAndTime) .sort() .map((key) { const sortedTimes [...selectedDateAndTime[key]].sort(); const firstSlotOfSelectedDay sortedTimes[0]; ...注意这里的[...selectedDateAndTime[key]].sort()Object.keys()本身返回新数组直接.sort()是安全的而对selectedDateAndTime[key]的值则先展开成[...]再排序原 state 中的时间数组保持不变。这与规则文档给出的 fallback 模式完全一致。浏览器支持范围与老环境降级方案.toSorted()属于 ES2023 的非变异复制方法copy method按规则文档给出的支持矩阵环境最低版本Chrome110Safari16Firefox115Node.js20对于需要兼容更老运行时的场景规则文档给出的等价降级写法是展开运算符// Fallback for older browsers const sorted [...items].sort((a, b) a.value - b.value)[...items].sort()与.toSorted()行为等价先浅拷贝一份再对新数组原地排序原数组不受影响。这也解释了上面 Cal.com 仓库现状——代码中普遍采用 spread 降级方案而非直接使用toSorted()在构建目标或浏览器覆盖范围不允许时这是行为完全一致的安全替换。完整清单同类不可变数组方法规则文档最后列出了 JavaScript 提供的整套非变异数组方法建议在团队内统一记忆方法对应变异版本语义.toSorted().sort()不可变排序.toReversed().reverse()不可变反转.toSpliced().splice()不可变截取/增删.with()无直接对应不可变替换指定位置的元素需要强调的是这四个方法与.sort()/.reverse()等一样都是浅拷贝——元素引用不变只复制了数组容器本身。对只读展示场景列表排序、展示顺序调整完全够用若元素本身也是可变异的对象仍需按 React 惯例创建新对象。小结把sort()换成toSorted()或[...].sort()降级写法是一次单行级、零行为副作用的替换却能消除一整类props/state 被就地改写、memo 失效、跨渲染状态错乱的隐性 bug。对照 Cal.com 仓库中的实际代码可以发现apps/web/modules/apps/components/下的滑块组件仍保留着原地排序 props 的写法而apps/web/modules/embed/components/Embed.tsx已经在使用先展开再排序的安全模式——这组真实对照恰好验证了规则文档的判断标准判断一个排序调用是否危险看它作用的对象是不是来自 props/state 且还会被其他渲染路径复用。【免费下载链接】cal.diyScheduling infrastructure for absolutely everyone.项目地址: https://gitcode.com/GitHub_Trending/ca/cal.diy创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考