记一次 React + Redux-form + Localstorage 返回到上一个页面无法编辑表单的 BUG

需求:

用 Redux-form 管理表单数据, 并且需要持久化整个 state 到 localstorage 里面, 这样在页面跳转后, 后面能够随时取到前面页面表单的数据.

代码实现概要:

表单用 redux-form 包裹:

// 因为刷新页面后, state 就没有上一个页面数据了, 必须合并 localstorage 存好的 state
// 其中 jso 是一个 npm 的库, 可以 override 前一个 object 的 element
const combineStateWithLocalstorage = state => jso(state, JSON.parse(localstorage.getItem('app')));

const mapStateToProps = state => ({
    // 这里是我们想要从前一个页面取的表单数据
    // getFormValues 是 redux-form 的方法
    previousFormValue: getFormValues('previousFormId')(combineStateWithLocalstorage(state)),
});

export default reduxForm({
    form: 'formId',
})(connect(mapStateToProps)(MyForm))

问题来了

我们的存储是在页面跳转的时候, 触发存储的, 但通过日志可以看出, 命名 state 上有数据, 但是 localstorage 就是没有存下来?

通过 redux-logger 看到了原因

其实 redux-form 为了实现垃圾回收机制, 所谓的 GC, 会在 component on unmount 的时候, 销毁表单, 也就是说页面跳转后, state 上对应 form 的表单就会被销毁. 存的时候自然就拿不到值了, 解决方案也很简单, 加上一个 destroyOnUnmount: false 即可.

export default reduxForm({
    form: 'formId',
    destroyOnUnmount: false,
})(connect(mapStateToProps)(MyForm))

 

重点来了

一切都很美好, 刷新页面也能重新填充 state 的值, 在任何一个页面我们都能取到任何一个表单的数据. 也能正常前后跳转, 数据也能正常重新回显.

直到有一次, 我们从页面 A 填好数据, 进入下一个页面 B, 再回到页面 A, 发现…. 表单居然无法编辑了, 数据怎么改都不会变化, 也就是无法输入新的值???

找原因

一开始, 我们认为是 localstorage 存的数据有变化, 我们通过日志发现, redux-form 其实在页面跳转的时候会 unregisterField 的, 然后 localstorage 就存下了像这样的数据:

{
  "app": {
    "formId": {
      "registerFields": {
        "someValue": {
          ...
          "count": 0
        }
      },
      "values": {
         ...
      }
    }
  }
}

你会发现, 有一个 count 是0, 正常情况下表单注册的字段, count 是1, 会不会是这个原因?

寻找有没有类似 NotUnregisterFieldsOnUnmount 的属性, 发现 redux-form 并没有. 只有 enableReinitialize 和 keepDirtyOnReinitialize 的属性, 但尝试了都无法解决问题.

尝试在各个 React component 的 lifesycle 方法重新 initialize 表单, 还是无法解决, 再次回到页面, 表单依旧无法再次编辑.

(n 小时鏖战…)

问题浮现

终于, 我们发现其实 在我们尝试编辑表单的时候, redux-form 会 dispatch 一个 CHANGE 的 action 出去, 这个 action 如果你断点跟着走, 会发现其实修改是成功了的, 但是又被 React-dom 的一个 JS 给强制回退回去了.

通过研究发现, React Redux 其实在每一次 action 被 dispatch 的时候, 都会重新执行一次 connect 里面的方法, 以保证 props 被修改后重新渲染页面.

那么问题就应该出在 connect 的方法里, 也就是我们写的 mapStateToProps 里面. 简单来说就是 redux-form 其实 dispatch 了 CHANGE 事件, 是成功修改了表单的, 但是紧接着又被 mapStateToProps 的什么东西给重新赋值回去了.

最终解决

我们发现 mapStateToProps 有这样的代码

jso(state, JSON.parse(localstorage.getItem('app')))

问题就出在这里, jso 是一个 用后一个 Object, 重新给前一个 Object赋值的方法, 就是合并2个 JSON Object. 仔细看源码发现有这样的代码

baseObject[key] = nextObject[key]

按照我们的用法, baseObject 就是 state, 也就是说 state 被改变了!!!!!

问题又来了

不是说改变 state, 需要用 setState 吗? 否则不生效!!!

这里却生效了, 有2个原因:

  1. 这里的 state 不是 React 的 state, 这个是由 Redux 管理的 state.
  2. 其实如果用引用修改这里的 state 是不生效的, 关键里面的代码是直接修改.

原因2 是这样的:

state = {someKey: someValue}; // 不生效
state.someKey = someValue; // 生效

到这里就找到原因了, 不用 jso 这个库即可解决问题!

发表评论

您的电子邮箱地址不会被公开。 必填项已用 * 标注

Scroll to Top