React 闭包陷阱:一个空依赖数组,毁了我的数据
前天晚上我正在给自己的开源项目 SkillLauncher Windows 版本收尾。这是一个帮助开发者快速启动 Claude Code Skills 的桌面工具。功能很简单点击某个技能卡片工具就会自动把技能名字复制到剪贴板同时记录下你使用了哪些技能、用了多少次。测试的时候一切正常。我用了一次「commit」再刷新页面使用记录还在。我又用了「pdf」刷新也还在。完美打包准备发版。第二天早上打开一测——所有的使用记录都不见了。空空如也的列表盯着我的脸那一刻我甚至怀疑自己是不是做梦记错了。问题一消失的数据我打开开发者工具定位到数据文件C:\Users\admin\AppData\Local\com.skillLauncher.app\skill-usage.json文件存在但内容是空的。为什么会这样让我带你看下原来的代码setUsageData((currentData){constnewData{usage:newUsage};// 异步保存(async(){awaitwriteFile(filePath,encoder.encode(JSON.stringify(newData)));})();returnnewData;});发现问题了吗直接覆盖写入没有考虑文件中已有的数据。但这还不是最致命的。更严重的问题在于加载逻辑asyncfunctionloadUsageData(){try{constdataJSON.parse(jsonStr);setUsageData(data);}catch(err){// 任何读取失败都会导致空数据setUsageData({usage:[]});// 危险}}这是一个时序炸弹loadUsageData读取失败可能是临时权限问题、文件被占用、JSON 解析错误内存状态被设为{ usage: [] }用户点击技能触发保存保存逻辑基于空数据计算然后用空数据覆盖文件文件中的有效数据永久丢失教训永远不要用空数据去覆盖可能有数据的文件。解决方案合并写入修复后的代码遵循一个原则先读后写合并而非覆盖。// 保存前先读取现有文件合并后再写入try{const{readFile}awaitimport(tauri-apps/plugin-fs);constexistingContentsawaitreadFile(filePath);constexistingDataJSON.parse(decoder.decode(existingContents));if(existingData?.usage?.length0){// 使用 Map 合并数据新记录覆盖旧的同名记录constmergedMapnewMapstring,SkillUsageRecord();// 先添加现有记录existingData.usage.forEach(record{if(record?.name){mergedMap.set(record.name,record);}});// 再添加新记录自动覆盖同名的newUsage.forEach(record{if(record?.name){mergedMap.set(record.name,record);}});constmergedData{usage:Array.from(mergedMap.values())};awaitwriteFile(filePath,encoder.encode(JSON.stringify(mergedData,null,2)));return;// 合并成功直接返回}}catch(readErr){// 文件不存在或读取失败创建新文件}// 正常保存新文件awaitwriteFile(filePath,encoder.encode(jsonStr));核心思想很简单保存前先读出文件里的旧数据和内存中的新数据合并然后再写回去。这样即使加载时出了问题文件里的数据也不会丢。问题二永远走不进去的 if修复完数据问题我以为可以收工了。结果又发现一个 bug我的「等待加载完成」逻辑从来没生效过。const[loadCompleted,setLoadCompleted]useState(false);constrecordUsageuseCallback(async(skillName:string){// 检查加载状态if(!loadCompleted){console.log(等待加载完成...);// 等待逻辑}// ...},[]);// 空依赖数组这段代码看起来没问题吧但实际运行时loadCompleted永远是false。即使我调用了setLoadCompleted(true)recordUsage函数里读到的还是false。这就是经典的React 闭包陷阱。为什么会这样useCallback的依赖数组为空时回调函数只在组件挂载时创建一次// 组件挂载时loadCompleted falseconstrecordUsageuseCallback((){console.log(loadCompleted);// 闭包捕获了 false},[]);// 空依赖永不更新// 即使后来 setLoadCompleted(true)// recordUsage 内部的 loadCompleted 仍然是 falseJavaScript 闭包捕获的是变量值而非变量引用。这就像你拍了一张照片之后无论被拍摄的人怎么换衣服照片里的样子永远不会变。解决方案useRef最简单的修复方式是用useRef// useRef 返回的对象在整个组件生命周期内保持不变constloadCompletedRefuseRef(false);// 修改值通过 .currentloadCompletedRef.currenttrue;// useCallback 内部访问 .currentconstrecordUsageuseCallback(async(skillName:string){if(!loadCompletedRef.current){// 总是获取最新值// 等待逻辑}},[]);useRef返回的是一个可变对象{ current: ... }。对象的引用在闭包中保持不变但通过.current访问的始终是最新值。这就像拍了一张视频而不是照片内容会实时更新。总结这两个问题花了我大半夜时间但也总结出一些经验数据持久化最佳实践先读后写保存前读取现有数据合并后再写入防御性检查验证数据格式避免用空数据覆盖错误恢复写入失败时保留原有数据React Hooks 避坑指南场景问题解决方案useCallback中变量值过期空依赖数组导致闭包捕获初始值使用useRefsetInterval中 state 过期定时器回调捕获初始 state使用 useRef事件监听器中 state 过期监听器闭包捕获旧值每次更新时重新添加监听器其实这些都不是什么高深的技巧只是在写代码时多想一步如果这里出错了会发生什么开源项目 SkillLauncher 已经发布欢迎体验https://github.com/gxj1134506645/skillLauncher-windows欢迎关注公众号 FishTech Notes一块交流使用心得

相关新闻

拖延症福音!降AIGC工具 千笔·专业降AI率智能体 VS 锐智 AI,本科生专属神器

拖延症福音!降AIGC工具 千笔·专业降AI率智能体 VS 锐智 AI,本科生专属神器

在AI技术迅速发展的今天,越来越多的本科生开始借助AI工具辅助论文写作,以提升效率和内容质量。然而,随着学术审查标准的不断提高,AI生成内容的痕迹愈发明显,查重系统对AIGC的识别也日趋精准。这使得许多学生陷入“用AI…

2026/7/3 14:12:59 阅读更多 →
专科生必看!千笔ai写作,标杆级的AI论文软件

专科生必看!千笔ai写作,标杆级的AI论文软件

你是否曾为论文选题发愁,反复修改却仍不满意?文献检索耗时费力,查重率又总是不达标?面对繁复的格式要求和时间压力,你是否感到力不从心?在学术写作的道路上,这些难题似乎成了每个学生的“必修课…

2026/7/4 16:43:54 阅读更多 →
生鲜配送商城APP前端功能设计:以便捷性守护新鲜体验

生鲜配送商城APP前端功能设计:以便捷性守护新鲜体验

生鲜消费对新鲜度与时效性的高需求,推动生鲜配送商城APP前端功能不断优化。其功能版块围绕用户核心体验构建,摒弃过度商业引导,以清晰逻辑衔接购物全流程,既通过细节设计保障生鲜品质感知,又以简化操作降低使用门槛&am…

2026/7/4 5:25:23 阅读更多 →

最新新闻

零日漏洞攻防实战:从检测到响应的纵深防御体系构建

零日漏洞攻防实战:从检测到响应的纵深防御体系构建

1. 项目概述:直面数字世界的“隐形杀手”在网络安全这个没有硝烟的战场上,最让防御者感到棘手的,往往不是那些已知的、有补丁可循的威胁,而是那些被称为“零日漏洞”的未知攻击。从业十几年,我处理过无数次安全事件&am…

2026/7/5 13:16:07 阅读更多 →
多人聊天室

多人聊天室

一、项目简介本项目是一个基于Java Swing MySQL的博客文章管理系统,实现了文章发布、分类管理、用户登录、全局搜索等核心功能。 我在项目中主要负责全局搜索模块、数据库读写层设计以及部分面向对象架构设计工作。二、个人任务简述序号完成功能与任务描述1全局搜索…

2026/7/5 13:14:06 阅读更多 →
骑乘无忧怎么选 (新手女生小个子巡航摩托)选购要点

骑乘无忧怎么选 (新手女生小个子巡航摩托)选购要点

入手自动挡巡航摩托,CVT 和 AMT 该怎么选?面向入门骑手、女性车友以及身高娇小的人群,最优方案已然明确。AMT 巡航操控顺手、动力充沛、使用便捷,外观也十分出彩,是综合实力更强的选择。QJMOTOR 闪 300AMT 与闪 400AMT…

2026/7/5 13:14:06 阅读更多 →
Azure Local离线模式采购(系列篇之七)

Azure Local离线模式采购(系列篇之七)

0. 重要定位(先看清 Acquire 在做什么) ⚠️ Acquire ≠ 部署完成。Acquire 阶段仅完成 Azure 资源创建及部署介质获取,Virtual Appliance 尚未部署到本地数据中心。完整的生命周期是: Acquire → Deploy → Configure → Operate…

2026/7/5 13:12:06 阅读更多 →
杭州老板IP打造运营公司怎么选?

杭州老板IP打造运营公司怎么选?

选择杭州的老板IP打造运营公司时,可以从以下几个方面进行考量:一、明确需求与目标核心需求:首先明确你希望通过IP打造实现什么目的。是增加品牌知名度、提升客户信任度,还是直接促进销售转化? 行业特性:根据…

2026/7/5 13:12:06 阅读更多 →
input_report_key + input_sync:按键事件的正确报告姿势

input_report_key + input_sync:按键事件的正确报告姿势

input_report_key input_sync:按键事件的正确报告姿势这个仓库已经开源!所有教程,主线内核移植,跑新版本imx-linux/uboot都在这里,或者一起来尝试跑7.1的Linux!欢迎各位大佬观摩!喜欢的话点个⭐…

2026/7/5 13:10:06 阅读更多 →

日新闻

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容 【免费下载链接】BiliTools A cross-platform bilibili toolbox. 跨平台哔哩哔哩工具箱,支持下载视频、番剧等等各类资源 项目地址: https://gitcode.com/GitHub_Trending/bilit/BiliTools …

2026/7/5 0:03:34 阅读更多 →
威胁模型全解析:从新手入门到实战应用,助你构建安全产品!

威胁模型全解析:从新手入门到实战应用,助你构建安全产品!

威胁模型的陌生现状在忙碌疲惫的一天里,参与了关于混合后量子密码学的讨论,应付端点攻击找茬的人,还参与留言板讨论后,发现“威胁模型”对多数人仍是陌生概念,且多被当作时髦用语。有趣的相关画作有一幅由 Embyr 创作的…

2026/7/5 0:03:34 阅读更多 →
渗透测试入门指南:从零基础到实战环境搭建

渗透测试入门指南:从零基础到实战环境搭建

1. 从“看热闹”到“入门”:我理解的渗透测试到底是什么?每次看到新闻里说某个大公司的数据被“黑”了,或者某个网站被攻击导致服务瘫痪,你是不是和我一样,心里会冒出两个念头:一是“这黑客真厉害”&#x…

2026/7/5 0:07:38 阅读更多 →

周新闻

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容

B站视频下载神器BiliTools:5分钟学会轻松保存任何B站内容 【免费下载链接】BiliTools A cross-platform bilibili toolbox. 跨平台哔哩哔哩工具箱,支持下载视频、番剧等等各类资源 项目地址: https://gitcode.com/GitHub_Trending/bilit/BiliTools …

2026/7/5 0:03:34 阅读更多 →
威胁模型全解析:从新手入门到实战应用,助你构建安全产品!

威胁模型全解析:从新手入门到实战应用,助你构建安全产品!

威胁模型的陌生现状在忙碌疲惫的一天里,参与了关于混合后量子密码学的讨论,应付端点攻击找茬的人,还参与留言板讨论后,发现“威胁模型”对多数人仍是陌生概念,且多被当作时髦用语。有趣的相关画作有一幅由 Embyr 创作的…

2026/7/5 0:03:34 阅读更多 →
渗透测试入门指南:从零基础到实战环境搭建

渗透测试入门指南:从零基础到实战环境搭建

1. 从“看热闹”到“入门”:我理解的渗透测试到底是什么?每次看到新闻里说某个大公司的数据被“黑”了,或者某个网站被攻击导致服务瘫痪,你是不是和我一样,心里会冒出两个念头:一是“这黑客真厉害”&#x…

2026/7/5 0:07:38 阅读更多 →

月新闻