semi-design icon indicating copy to clipboard operation
semi-design copied to clipboard

[ArrayField] when calling addWithInitValue in async function, UI not rerender

Open pointhalo opened this issue 2 years ago • 4 comments

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 补充说明

pointhalo avatar Mar 23 '22 04:03 pointhalo

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

pointhalo avatar Aug 30 '22 08:08 pointhalo

这个bug啥时候能fix

gitHber avatar May 29 '23 14:05 gitHber

这个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>
        );
    }
}

pointhalo avatar May 30 '23 12:05 pointhalo

补充: 在下面这个例子中,需要将 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>
        );
    }
}

DaiQiangReal avatar Jun 14 '23 03:06 DaiQiangReal