alibaba-fusion / next

🦍 A configurable component library for web built on React.
https://fusion.design
MIT License
4.58k stars 587 forks source link

【Field】将field作为props传递给子组件,同时子组件用mobx-react-lite的observer 方法包裹时,子组件onChange不生效 #4411

Closed chi-gao closed 11 months ago

chi-gao commented 1 year ago

背景

父组件创建field实例, 然后将field作为props传递给子组件, 同时引入Mobx作为状态管理, 因此子组件需要使用observer包裹,以响应 mobx store的变化。

现象

子组件中的表单项,onChange时Field中的对应值变化了,但是表单本身值没有变化,应该是setState未生效。如下图: image

复现地址

https://stackblitz.com/edit/stackblitz-starters-mvmzph?file=src%2FApp.tsx

步骤

直接切换下拉框的选项即可。

预期结果

正确响应onChange事件。

eternalsky commented 1 year ago

field 的 onChange 在你的 demo 里我看已经生效了,是你的子组件没有重新渲染去拿到最新的 field 值

chi-gao commented 1 year ago

是的, 正常情况(没有使用observer)时,应该是 onChange的时候同时触发子组件的重新渲染。 但使用了observer之后, 重新渲染好像不生效了。

所以请教一下,在使用mobx的场景下, 应如何确保子组件的正常渲染?

eternalsky commented 1 year ago

跟 mobx 没关系,你这里没有 mobx 也不会自动渲染Child,因为 field 拿到的是 Select,他只会让 Select 重新渲染,但是 Select 拿到的 props 是 Child 传给他的,但是 Child 并没有重新渲染。

eternalsky commented 1 year ago

本质是你 field 传入方式不对,你应该是让 Child 去接收 field.init 出来的属性,而不是里面的 Select

chi-gao commented 1 year ago

因为业务本身比较复杂,field表单分布在不同的子组件中,因此只能将field本身作为props传给各个子组件,这个是业务需要。 然后看了下field和 mobx-react-liteobserver 的代码,搞明白了原因。

首先,为什么没有使用observer 包裹的组件可以响应 field的变化?

原因是当使用 field.init 初始化后的组件触发onChange时, 会触发初始化field的组件(这里是父组件)的 reRender;然后父组件的reRender会触发子组件的reRender,因此子组件可以正常响应field值的变化,进而正确渲染。代码参考这里这里 image

其次,为什么使用observer 包裹之后不响应field的变化了? 原因是 mobx-react-lite中的observer 方法为了性能考虑,会使用 React.memo包裹组件,包裹后的组件只有props变化才会触发重新渲染,因此父组件的reRender不会再引起子组件的reRender,因此无法响应正确的field值变化。代码参考这里 image

问题搞清楚了,那么怎么解呢? 其实不管是field,还是mobx-react-lite都没问题,可能确实不支持这种直接传递field的场景。最终原因是 子组件中表单项值变化只触发了父组件的重新渲染,子组件本身的props木有变化,因此重新渲染被阻断了。那么当表单项值变化时,同时引起子组件的props变化是不是就可以了?

基于这个思路,我们在父组件引入一个新的state,然后将这个state传给子组件用来响应field值的变化。最新代码参考这里:https://stackblitz.com/edit/stackblitz-starters-mvmzph?file=src%2FChild.tsx,src%2FApp.tsx image 最终效果如下: image

----------------------我是分割线-------------------------- 到这里为止, 问题是解了,但是方法还是相对比较tricky, 还是希望field本身能够支持这种用法,感谢感谢

eternalsky commented 11 months ago

我们无法 100% 控制子组件是否渲染,只能让 field 本身 attach 的组件进行重新渲染。比如你说的 props 变化触发子组件渲染,这只是因为你的子组件会对这个 props 变化产生重新渲染的行为,但不代表所有的子组件都是这样处理的。因为部门不知道子组件的shouldComponentUpdate 是怎样写的。

chi-gao commented 11 months ago

我们无法 100% 控制子组件是否渲染,只能让 field 本身 attach 的组件进行重新渲染。比如你说的 props 变化触发子组件渲染,这只是因为你的子组件会对这个 props 变化产生重新渲染的行为,但不代表所有的子组件都是这样处理的。因为部门不知道子组件的shouldComponentUpdate 是怎样写的。

是的,所以传递field实例本身可能不是一个好的实践。我这个例子我感觉正确的写法应该是:每个子组件包含自己的field,子组件利用value+onChange可以作为一个自定义的手控组件受父组件的控制