已有localStorage/sessionStorage时为何要使用Redux/useContext?
首先要明确:状态持久化根本不是Redux、useContext(配合React Context使用)的原生核心能力——原生Redux和Context存在内存里,页面刷新状态就会清空,所谓的持久化是需要搭配redux-persist这类第三方库、或者手写读写逻辑才能实现的附加功能,二者的核心设计目标从一开始就是解决组件树内的状态共享问题,和localStorage、sessionStorage(以下统称Web Storage)的定位完全不一样。
Web Storage虽然能存值、能跨同域页面读值,但它本质是浏览器提供的静态存储接口,和React的运行时完全脱节,根本替代不了状态管理库,核心缺失的能力有这几个:
1. 原生响应式绑定,自动关联UI渲染
Web Storage的内容变更不会主动触发React组件重渲染,甚至你连当前页面内的存储变更都监听不到——浏览器自带的storage事件只会在其他同域标签页修改存储值时触发,当前页面自己改localStorage/sessionStorage,绑定的监听回调根本不会执行。你要是想靠Web Storage实现跨组件状态同步,得自己写一套事件发布订阅、手动触发组件强制刷新的逻辑,漏一处就会出现“值已经改了但页面没更新”的bug。
而Redux、Context的状态是直接接入React渲染流程的,只要状态更新,所有订阅了对应状态的组件会自动触发重渲染,不需要你写任何胶水代码,UI和状态始终保持同步。
2. 可管控的状态更新流程,全链路可追溯
Web Storage是完全开放的全局接口,页面上跑的任何一段JS代码都能随意读写、删除里面的值,没有任何校验、拦截机制,一旦出现状态异常,你根本查不出来是哪段代码、在什么时机改坏了数据。
Redux从设计上就要求所有状态修改必须通过dispatch提交action走统一的更新链路,你可以很方便的加中间件做操作日志、权限校验、非法值拦截,每一次状态变更的触发源、变更前后的值都能完整追溯。就算是用useContext,你也可以把状态修改的逻辑封装在Provider内部,只对外暴露有限的合法修改方法,从根源上避免状态被随意篡改。
3. 内存级读写性能,无额外使用限制
Web Storage的读写是同步阻塞操作,而且只支持存字符串类型,存对象、数组这类结构化数据必须手动做JSON.stringify序列化,读取的时候还要做JSON.parse反序列化,数据量大、读写频繁的时候很容易卡住主线程;同时Web Storage一般只有5-10M的容量上限,存不了太多数据,也不支持存函数、Symbol、DOM节点、带循环引用的对象这类特殊值。
Redux和Context的状态是存在运行时内存中的,读写不需要走IO操作,也没有序列化/反序列化的开销,性能比Web Storage高几个量级,同时支持存储任意合法的JS类型值,也没有严格的容量限制,完全贴合前端开发的使用场景。
4. 细粒度的订阅能力,减少无效渲染
Web Storage没有细粒度订阅的能力,你要么监听所有存储变更,要么完全不监听,根本做不到“只关心我用到的某一个字段,其他值变了和我无关”。
Redux原生支持组件只订阅自己依赖的那部分状态,比如一个组件只用到购物车的商品总数,那只有总数变化的时候才会触发重渲染,全局其他状态怎么改都不会影响它。就算是用useContext,你也可以通过拆分多个Context、配合useMemo做状态缓存,实现精细化的更新控制,避免无关组件跟着重渲染拖慢性能。
5. 对React生态的原生适配
Web Storage和React的渲染流程、生命周期完全割裂,你得自己处理SSR场景下服务端没有window对象的兼容问题、自己处理组件卸载时的事件监听清理、自己处理首屏注水时状态和服务端渲染内容对齐的问题,实际用起来坑非常多。
Redux和useContext本身就是为React设计的,天然兼容SSR、并发模式等所有React新特性,还能直接对接React DevTools,在调试面板里直接查看当前状态、回溯状态变更历史,不需要自己额外搭调试工具。
实际项目里二者从来不是互斥的关系:一般会用Redux/Context做运行时的状态管理,负责驱动UI渲染,同时搭配Web Storage做持久化备份,页面初始化的时候从Web Storage里读之前存的值做状态恢复,各司其职。
内容的提问来源于stack exchange,提问作者Bear Bile Farming is Torture

