semi-design
semi-design copied to clipboard
[ArrayField] when calling addWithInitValue in async function, UI not rerender
Which Component 出现bug的组件
- Form.ArrrayField
semi-ui version
- 2.6.0
Expected result 期望的结果是什么
- after upload onSuccess, add a new line,both formState & ui update, arrayFIeld has 3 lines
Actual result 实际的结果是什么
- formState has update
- but UI not update,still only two lines
Steps to reproduce 复现步骤
- select some file to upload
- onSuccess will be called
Reproducible code 复现代码
class ArrayFieldDemo extends React.Component {
constructor() {
super();
this.state = {
menu: [
{ name: '脸部贴纸', type: '2D' },
{ name: '前景贴纸', type: '3D' },
]
}
}
render() {
let { menu } = this.state;
const ComponentUsingFormState = () => {
const formState = useFormState();
return (
<TextArea style={{ marginTop: 10 }} value={JSON.stringify(formState)} />
);
};
return (
<Form style={{ width: 500 }} labelPosition='left' labelWidth='220px' allowEmpty>
<ArrayField field='effects' initValue={menu}>
{({ add, arrayFields, addWithInitValue }) => (
<React.Fragment>
<Upload
action="//[semi.bytedance.net/api/upload](http://semi.bytedance.net/api/upload)"
onSuccess={() => {
console.log('success');
+ addWithInitValue({ name: '自定义贴纸', type: '2D' })}}
>
<Button icon="upload" theme="light">
点击上传
</Button>
</Upload>
<Button onClick={add} icon='plus_circle' theme='light'>新增空白行</Button>
<Button icon='plus_circle' onClick={() => addWithInitValue({ name: '自定义贴纸', type: '2D' })} style={{ marginLeft: 8 }}>新增带有初始值的行</Button>
{
arrayFields.map(({ field, key, remove }, i) => (
<div key={key} style={{ width: 1000, display: 'flex' }}>
<Form.Input
field={`${field}[name]`}
label={`特效类型:(${field}.name)`}
style={{ width: 200, marginRight: 16 }}
>
</Form.Input>
<Form.Select
field={`${field}[type]`}
label={`素材类型:(${field}.type)`}
style={{ width: 90 }}
>
<Form.Select.Option value='2D'>2D</Form.Select.Option>
<Form.Select.Option value='3D'>3D</Form.Select.Option>
</Form.Select>
<Button type='danger' theme='borderless' icon="minus_circle" onClick={remove} style={{ margin: 12 }}></Button>
</div>
))
}
</React.Fragment>
)}
</ArrayField>
<ComponentUsingFormState />
</Form>
);
}
}
Additional information 补充说明
one more demo
class AsyncSetArrayField extends React.Component {
constructor() {
super();
this.state = {
data: [
{ name: 'Semi D2C', role: 'Engineer' },
{ name: 'Semi C2D', role: 'Designer' },
]
};
}
getFormApi = (formApi) => {
this.formApi = formApi;
}
change = () => {
let rules = this.formApi.getValue('rules');
if (!rules) {
rules = [];
}
rules.push({ name: Math.random(), role: 'Designer', key: this.id++ });
+ setTimeout(() => {
this.formApi.setValue('rules', rules);
+ }, 2000);
}
render() {
let { data } = this.state;
const ComponentUsingFormState = () => {
const formState = useFormState();
return (
<TextArea style={{ marginTop: 10 }} value={JSON.stringify(formState)} />
);
};
return (
<Form style={{ width: 800 }} labelPosition='left' labelWidth='100px' allowEmpty getFormApi={this.getFormApi}>
<ArrayField field='rules' initValue={data}>
{({ add, arrayFields, addWithInitValue }) => (
<React.Fragment>
<Button onClick={this.change} theme='light'>change</Button>
<Button onClick={add} icon={<IconPlusCircle />} theme='light'>Add new line</Button>
<Button icon={<IconPlusCircle />} onClick={() => {addWithInitValue({ name: 'Semi DSM', type: 'Designer' });}} style={{ marginLeft: 8 }}>Add new line with init value</Button>
{
arrayFields.map(({ field, key, remove }, i) => (
<div key={key} style={{ width: 1000, display: 'flex' }}>
<Form.Input
field={`${field}[name]`}
label={`${field}.name`}
style={{ width: 200, marginRight: 16 }}
>
</Form.Input>
<Form.Select
field={`${field}[role]`}
label={`${field}.role`}
style={{ width: 120 }}
optionList={[
{ label: 'Engineer', value: 'Engineer' },
{ label: 'Designer', value: 'Designer' },
]}
>
</Form.Select>
<Button
type='danger'
theme='borderless'
icon={<IconMinusCircle />}
onClick={remove}
style={{ margin: 12 }}
/>
</div>
))
}
</React.Fragment>
)}
</ArrayField>
<ComponentUsingFormState />
</Form>
);
}
}
if call setValue async, arrayField not rerender as expected
这个bug啥时候能fix
这个bug啥时候能fix
这个bug的根本原因是当前 ArrayField 的更新机制不太合理,依赖于父组件的 forceUpdate。而forceUpdate如果被 setTimeout 包裹 或者 处于 async callback 中的调用链中时,子组件的 componentDidUpdate的触发时机,会与非异步时有区别。 所以 涉及 ArrayField 更新机制的重新设计,改动点比较多 ,我这边已经有一个分支在跟进这个问题 https://github.com/DouyinFE/semi-design/commits/refactor-ArrayFieldUpdate ,但整体需要回归的case很多,也有一些新产生的bug未完全 fix,暂时还没有明确的提PR的时间。
在未修复之前,暂时可以先通过强制 父组件 rerender一次,来作为临时方案绕过这个问题
eg
class ArrayFieldDemo extends React.Component {
constructor() {
super();
this.state = {
menu: [
{ name: '脸部贴纸', type: '2D' },
{ name: '前景贴纸', type: '3D' },
]
}
}
render() {
let { menu } = this.state;
const ComponentUsingFormState = () => {
const formState = useFormState();
return (
<TextArea style={{ marginTop: 10 }} value={JSON.stringify(formState)} />
);
};
return (
<Form style={{ width: 500 }} labelPosition='left' labelWidth='220px' allowEmpty>
<ArrayField field='effects' initValue={menu}>
{({ add, arrayFields, addWithInitValue }) => (
<React.Fragment>
<Button
icon="upload"
theme="light"
onClick={()=>{
setTimeout(() => {
addWithInitValue({ name: '自定义贴纸', type: '2D' })
+ this.setState({ someKey: Math.random() })
}, 100)
}}
>
addAsync
</Button>
<Button onClick={add} icon='plus_circle' theme='light'>新增空白行</Button>
<Button icon='plus_circle' onClick={() => addWithInitValue({ name: '自定义贴纸', type: '2D' })} style={{ marginLeft: 8 }}>新增带有初始值的行</Button>
{
arrayFields.map(({ field, key, remove }, i) => (
<div key={key} style={{ width: 1000, display: 'flex' }}>
<Form.Input
field={`${field}[name]`}
label={`特效类型:(${field}.name)`}
style={{ width: 200, marginRight: 16 }}
>
</Form.Input>
<Form.Select
field={`${field}[type]`}
label={`素材类型:(${field}.type)`}
style={{ width: 90 }}
>
<Form.Select.Option value='2D'>2D</Form.Select.Option>
<Form.Select.Option value='3D'>3D</Form.Select.Option>
</Form.Select>
<Button type='danger' theme='borderless' icon="minus_circle" onClick={remove} style={{ margin: 12 }}></Button>
</div>
))
}
</React.Fragment>
)}
</ArrayField>
<ComponentUsingFormState />
</Form>
);
}
}
补充: 在下面这个例子中,需要将 setValue 放到 setState 的 callback 中 才会正常
import React from 'react';
import { ArrayField, TextArea, Form, Button, useFormState } from '@douyinfe/semi-ui';
import { IconPlusCircle, IconMinusCircle } from '@douyinfe/semi-icons';
class ArrayFieldDemo extends React.Component {
constructor() {
super();
this.state = {
data: [
{ name: 'Semi D2C', role: 'Engineer' },
{ name: 'Semi C2D', role: 'Designer' },
],
test:1
};
}
render() {
let { data } = this.state;
const ComponentUsingFormState = () => {
const formState = useFormState();
return (
<TextArea style={{ marginTop: 10 }} value={JSON.stringify(formState)} />
);
};
return (
<Form style={{ width: 800 }} labelPosition='left' labelWidth='100px' allowEmpty getFormApi={api=>this.formApi = api}>
<ArrayField field='rules' initValue={data}>
{({ add, arrayFields, addWithInitValue }) => (
<React.Fragment>
<Button onClick={add} icon={<IconPlusCircle />} theme='light'>Add new line</Button>
<Button icon={<IconPlusCircle />} onClick={() => {addWithInitValue({ name: 'Semi DSM', type: 'Designer' });}} style={{ marginLeft: 8 }}>Add new line with init value</Button>
{
arrayFields.map(({ field, key, remove }, i) => (
<div key={key} style={{ width: 1000, display: 'flex' }}>
<Form.Input
field={`${field}[name]`}
label={`${field}.name`}
style={{ width: 200, marginRight: 16 }}
>
</Form.Input>
<Form.Select
field={`${field}[role]`}
label={`${field}.role`}
style={{ width: 120 }}
optionList={[
{ label: 'Engineer', value: 'Engineer' },
{ label: 'Designer', value: 'Designer' },
]}
>
</Form.Select>
<Button
type='danger'
theme='borderless'
icon={<IconMinusCircle />}
onClick={remove}
style={{ margin: 12 }}
/>
</div>
))
}
</React.Fragment>
)}
</ArrayField>
<ComponentUsingFormState />
<Button onClick={()=>{
setTimeout(()=>{
// this.formApi.setValue('rules',[]);
this.setState({test:Math.random()},()=>{
this.formApi.setValue('rules',[{}]);
})
})
}}
/>
</Form>
);
}
}