You need to enable JavaScript to run this app. React Redux存储Input Ref到状态后删除行报只读属性current错误
优惠活动
大模型
产品
解决方案
定价
更多

React Redux存储Input Ref到状态后删除行报只读属性current错误

报错根源

这个问题完全是设计冲突导致的,没有玄学bug:

  • Redux从设计上就要求全局状态里只能存放可序列化的纯JS值,Ref对象、DOM节点、组件实例这类带原生引用的非纯值,根本不允许存在state里。你把Ref存进Redux之后,搭配项目里使用的Immer不可变库,整个被纳入状态树的对象都会被包裹成只读代理,所有属性都禁止直接修改。
  • React卸载对应Input组件的时候,会主动把绑定的Ref对象的current设为null做清理。这时候这些Ref已经因为进入Redux状态树,处于Immer的只读保护范围里,无法修改对应属性,自然就抛出了Cannot assign to read only property 'current'的错误。

你之前的排查结果完全可以印证这个逻辑:

  • 不把Ref存入Redux:Ref只存在于组件局部作用域,没有被Immer做只读包裹,React修改current属性不受限制,肯定不会报错
  • 注释掉删除时更新rows、或者注释掉添加Ref的reducer逻辑:Ref从来没被纳入Redux/Immer的代理范围,还是原生可写的对象,自然也不会触发报错
具体修复步骤

核心原则记死:永远不要把React生成的Ref对象直接塞进Redux全局状态,按下面调整即可:

  • 先把Redux里跟Ref存储相关的逻辑全删掉:
    • 删掉PHONE_DATA_ADD_REF对应的action定义、reducer里的处理分支
    • 删掉initialState里的refData字段,彻底把Ref映射移出全局状态
  • 换个不被Redux代理的位置存储Ref映射
    你当初想把Ref统一放Redux,无非是想快速拿到指定行、指定列的输入框实例,做聚焦、取值、校验之类的操作,这个需求根本不需要用Redux实现:
    • 直接在Table组件层定义一个普通的JS对象(不是React state、不进Redux),用来存储Ref映射,结构比如inputRefMap = { [rowId]: { [colIndex]: 对应Input的ref } }
    • 把这个map的注册、清理方法通过Context或者props传到下层的Row、Input组件:Input挂载的时候把自己的Ref注册到这个map里,卸载的时候自动清理对应引用。这个普通JS对象是完全暴露在组件作用域里的,Immer根本不会去包裹它做只读代理,React清理Ref的current属性时没有任何阻碍。
    • 要是嫌传Ref麻烦,更简单的实现方案:给每个Input加唯一的data-row-id、data-col-index属性,需要获取实例的时候直接通过DOM查询定位即可,连手动维护Ref映射的代码都可以省掉。
  • 顺手修正现有代码里的潜在问题:
    • Row组件里用useMemo缓存createRef()结果的写法存在bug:useMemo的依赖是空数组,但返回值是带!top条件判断的结果,当组件的top属性变化时,Memo根本不会重算,拿到的都是旧的错误Ref引用。而且本身createRef就不适合用useMemo缓存,配合上面说的父层统一管理Ref的方案,这段逻辑可以直接删掉。
    • Reducer里写Immer逻辑的时候,不要直接拿外层原始state的值给draft赋值:应该直接基于draft本身做过滤操作,比如把现在的draft.refData = state.refData.filter(...)、draft.data.rows = state.data.rows.filter(...)改成过滤draft上的对应属性,不然时间久了容易出现状态同步偏差。

内容的提问来源于stack exchange,提问作者Dorjd

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.29 12:06:04