简体   繁体   中英

How to escape infinite loop of useState hook in React?

I am new to react and learning about hooks but I cannot escape from useState at second execution.

My caller function is calling another component called QueryPanel with parameter:

const [availables, setAvailable] = useState<string[]>([]);
useEffect(() => {
    context.services.records.getAvailablesList()
        .then((resp) => {
            if(resp != undefined) {
                setAvailables(resp);
            }
        });
}, []);

return <Panel1 text={availables}></Panel1>;

So Panel1 is opening with a string array or an empty array. What I want to do is that show dropdown and if the parameter is not empty then show first item as default item or if the parameter list is empty then show a string as a default value "No option available"

export const QueryPanel = (props: any) => {
    const t = useTrans();
    const context = useContext(AppContext);
    const [form] = Form.useForm<Store>();
    const [defaultValue, setDefaultValue] = useState("asd");

    const { Option } = Select;

    const onFinish = (values: Store) => {
        const queryParams: Query = {
            system: values.systemType,
            startDate: values.startDate,
            endDate: values.endDate,
            startFrequency: values.startFrequency,
            endFrequency: values.endFrequency
        };
        context.services.records.query(queryParams);
    };

    const validateOnFormChange = () => {
        form.validateFields();
    };

    const onFinishFailed = (errorInfo: ValidateErrorEntity) => {
        console.log('Failed:', errorInfo);
    };

// useEffect(() => {
//
//     if (props.text.length !== 0) {
//         setDefaultValue(props.text[0]);
//     }
//     else {
//         setDefaultValue("No option available");
//     }
// }, []);

// if (props.text.length > 0) {
//     setDefaultDbValue(props.text[0]);
// }
// else {
//     let defaultDropDownOption:string = "No Db Available";
//     setDefaultDbValue(defaultDropDownOption);
// }

My if-else is stuck in infinite loop because whenever I set default value to a value then it goes to same if statement again. How can I escape from it without using extra field? useEffect is commented out here and the weird thing is when i check the parameter prop.text it is undefined at beginning and thats why the code is not going inside if-else in useEffect and then at second iteration the prop.text is coming as true.

I have tried something with useEffect hook but did not work:

useEffect(() => {
        if (props.text.length > 0) {
            setDefaultValue(props.text[0]);
        }
    }, [defaultValue]);

But this time default db is coming empty

Here is the complete code of Panel1 (QueryPanel) but the some names are different

import { Button, Col, DatePicker, Form, InputNumber, Row, Select } from 'antd';
import { Store } from 'antd/lib/form/interface';
import { ValidateErrorEntity } from 'rc-field-form/lib/interface';
import React, {useContext, useEffect, useState} from 'react';
import { AppContext } from '../../../../shared/Context';
import './QueryPanel.less';
import { Query } from '../../../../rest/BackendApi';
import { SystemType } from '../../../../shared/models/SystemType';
import { useTrans } from "../../../../shared/i18n/i18n";


export const QueryPanel = (props: any) => {
    const t = useTrans();
    const context = useContext(AppContext);
    const [form] = Form.useForm<Store>();
    const [defaultDbValue, setDefaultDbValue] = useState("Select Db");

    const { Option } = Select;

    const onFinish = (values: Store) => {
        const queryParams: Query = {
            system: values.systemType,
            startDate: values.startDate,
            endDate: values.endDate,
            startFrequency: values.startFrequency,
            endFrequency: values.endFrequency
        };
        context.services.records.query(queryParams);
    };

    const validateOnFormChange = () => {
        form.validateFields();
    };

    const onFinishFailed = (errorInfo: ValidateErrorEntity) => {
        console.log('Failed:', errorInfo);
    };

    // useEffect(() => {
    //     if (props.text.length !== 0) {
    //         setDefaultDbValue(props.text[0]);
    //     }
    //     else {
    //         setDefaultDbValue("No db available");
    //     }
    // }, []);
    useEffect(() => {
        if (!props.text) return;
        if (props.text.length > 0) {
            setDefaultDbValue(props.text[0]);
        } 
        else setDefaultDbValue("No Db Available");
    }, [props.text?.length]);
    return (
        <div>
            <Form form={form} onFinish={onFinish} onFinishFailed={onFinishFailed} onValuesChange={validateOnFormChange}>
                <Row justify={"space-between"} >
                    <Col span={4}>
                        <Form.Item label={t.trans.queryPanel.systemType} name='systemType' className={"tay-inline-form-item"}>
                            <Select defaultValue={defaultDbValue}>
                                {props.text.map((element: any) => (
                                    <Option key={element} value={element}>
                                        {element}
                                    </Option>
                                ))}
                            </Select>
                        </Form.Item>
                    </Col>

                    <Col span={4}>
                        <Form.Item label={t.trans.queryPanel.startDate} name='startDate' className={"tay-inline-form-item"}
                            rules={[
                                ({ getFieldValue }) => ({
                                    validator(rule, value) {
                                        if (!value || !getFieldValue('endDate') || getFieldValue('endDate') >= value) {
                                            return Promise.resolve();
                                        }
                                        return Promise.reject(t.trans.queryPanel.errors.startDate);
                                    },
                                }),
                            ]}>
                            <DatePicker format="DD-MM-YYYY" />
                        </Form.Item>

                    </Col>
                    <Col span={4}>
                        <Form.Item label={t.trans.queryPanel.endDate} name='endDate' className={"tay-inline-form-item"}
                            rules={[
                                ({ getFieldValue }) => ({
                                    validator(rule, value) {
                                        if (!value || !getFieldValue('startDate') || getFieldValue('startDate') <= value) {
                                            return Promise.resolve();
                                        }
                                        return Promise.reject(t.trans.queryPanel.errors.endDate);
                                    },
                                }),
                            ]}>
                            <DatePicker format="DD-MM-YYYY" />
                        </Form.Item>
                    </Col>

                    <Col span={4}>
                        <Form.Item label={t.trans.queryPanel.startFrequency} name='startFrequency' className={"tay-inline-form-item"}
                            rules={[
                                ({ getFieldValue }) => ({
                                    validator(rule, value) {
                                        if (!value) return Promise.resolve();
                                        if (value < 0 || value > 200) {
                                            return Promise.reject(t.trans.queryPanel.errors.startFrequency);
                                        } else if (getFieldValue('endFrequency') && getFieldValue('endFrequency') < value) {
                                            return Promise.reject(t.trans.queryPanel.errors.startFrequencyValues);
                                        }
                                        return Promise.resolve();
                                    },
                                }),
                            ]}>
                            <InputNumber />
                        </Form.Item>

                    </Col>
                    <Col span={4}>
                        <Form.Item label={t.trans.queryPanel.endFrequency} name='endFrequency' className={"tay-inline-form-item"}
                            rules={[
                                ({ getFieldValue }) => ({
                                    validator(rule, value) {
                                        if (!value) return Promise.resolve();
                                        if (value < 0 || value > 400) {
                                            return Promise.reject(t.trans.queryPanel.errors.endFrequency);
                                        } else if (getFieldValue('startFrequency') && getFieldValue('startFrequency') > value) {
                                            return Promise.reject(t.trans.queryPanel.errors.endFrequencyValues);
                                        }
                                        return Promise.resolve();
                                    },
                                }),
                            ]}>
                            <InputNumber />
                        </Form.Item>
                    </Col>

                    <Col span={2}>
                        <Form.Item className={"tay-inline-form-item"}>
                            <Button type="primary" htmlType="submit">
                                {t.trans.queryPanel.search}
                            </Button>
                        </Form.Item>
                    </Col>
                </Row>
            </Form>

        </div>
    );
}

I believe that one way would be to do:

useEffect(() => {
        if (props.text.length > 0 && props.text[0] !== defaultValue) {
            setDefaultValue(props.text[0]);
        }
    }, [defaultValue]);

By doing so, you will update the defaultValue only when it differs from props.text[0]

The infinite loop is created because React will rerender your <Panel1 /> component whenever the state of the component changes.

In the body of your component you have this if... else statement sitting without wrapping it in a hook:

if (props.text.length > 0) {
    setDefaultDbValue(props.text[0]);
} else {
    let defaultDropDownOption:string = "No Db Available";
    setDefaultDbValue(defaultDropDownOption);
}

So, either way you will execute setDefaultDbValue() which will update the state and trigger a rerender.

To prevent this loop you can wrap this snipped with useEffect() and use your text.length as a dependency:

useEffect(() => {
    if (!props.text) return; // wait until it is defined.
    if (props.text.length > 0) setDefaultDbValue(props.text[0]);
    else setDefaultDbValue("No Db Available");
}, [props.text?.length]);

There is no need to pass defaultValue as dependency to the useEffect hook. Or do I missunderstand your intention with this?

This aside: the better way to set default values is to do it in the useState(<DEFAULT_VALUE>) hook directly. Just the way you did it. Why is this not an option for you?

Edit

After your edit it is clear that the issue lies in how Ant uses the prop defaultValue . This prop is not reactive - so updating it after the Select has been rendered has no effect.

So you can either wait for the request to resolve before rendering your Panel, or:

If you want to select the first element of your text prop, once it is loaded, you have to set the value of the Select accordingly. Note that you have to update the value via onChange handler, if you bind the value to a state variable.

Here an example:

 const Select = antd.Select; const values = [ 'one', 'two', 'three', ]; // This is just a dummy representing your `text` prop const lazyValues = new Promise((res) => { setTimeout(() => { res(values); }, 2000); }); const App = () => { const [text, setText] = React.useState([]); const [value, setValue] = React.useState(); React.useEffect(() => { // fake the request lazyValues.then((values) => setText(values)); }, []); // if `text` changes its value, set the Select to its first element React.useEffect(() => { if (text.length > 0 &&;value) setValue(text[0]), }; [text]). return <div> <Select onChange={setValue /* Need to update our value manually */} defaultValue={"Default value visible if there is no value"} value={value}> {text,map((text. i) => <Select.Option key={i} value={text}>{text}</Select.Option>)} </Select> </div> } ReactDOM,render(<App />. document;getElementById('app'));
 <script crossorigin src="https://unpkg.com/react@17/umd/react.development.js"></script> <script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script> <script crossorigin src="https://unpkg.com/antd@4.17.4/dist/antd.min.js"></script> <link rel='stylesheet' type="text/css" href="https://unpkg.com/antd@4.17.4/dist/antd.min.css"></link> <div id="app"/>

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM