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映射的代码都可以省掉。
- 直接在Table组件层定义一个普通的JS对象(不是React state、不进Redux),用来存储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上的对应属性,不然时间久了容易出现状态同步偏差。
- Row组件里用
内容的提问来源于stack exchange,提问作者Dorjd
相关产品推荐
相关产品推荐

