如何在 React 中逃避 useState 钩子的无限循环?
How to escape infinite loop of useState hook in React?
我是 React 和学习 hooks 的新手,但我无法在第二次执行时摆脱 useState。
我的调用函数正在调用另一个名为 QueryPanel 的组件,参数为:
const [availables, setAvailable] = useState<string[]>([]);
useEffect(() => {
context.services.records.getAvailablesList()
.then((resp) => {
if(resp != undefined) {
setAvailables(resp);
}
});
}, []);
return <Panel1 text={availables}></Panel1>;
所以 Panel1 以字符串数组或空数组打开。我想要做的是显示下拉列表,如果参数不为空,则将第一项显示为默认项,或者如果参数列表为空,则将字符串显示为默认值“无可用选项”
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);
// }
我的 if-else 陷入了无限循环,因为每当我将默认值设置为一个值时,它就会再次进入相同的 if 语句。如何在不使用额外字段的情况下摆脱它? useEffect 在这里被注释掉了,奇怪的是当我检查参数 prop.text 它在开始时是 undefined 这就是代码的原因不会在 useEffect 中进入 if-else,然后在第二次迭代中 prop.text 变为 true。
我尝试过使用 useEffect 钩子但没有成功:
useEffect(() => {
if (props.text.length > 0) {
setDefaultValue(props.text[0]);
}
}, [defaultValue]);
但是这次默认数据库变空了
这里是Panel1(QueryPanel)的完整代码,只是有些名字不同
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={"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={"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={"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={"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={"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={"inline-form-item"}>
<Button type="primary" htmlType="submit">
{t.trans.queryPanel.search}
</Button>
</Form.Item>
</Col>
</Row>
</Form>
</div>
);
}
我认为一种方法是:
useEffect(() => {
if (props.text.length > 0 && props.text[0] !== defaultValue) {
setDefaultValue(props.text[0]);
}
}, [defaultValue]);
这样,您将仅在 defaultValue
不同于 props.text[0]
时更新它
创建无限循环是因为只要组件状态发生变化,React 就会重新渲染您的 <Panel1 />
组件。
在您的组件主体中,您有这个 if ... else
语句,但没有将其包裹在钩子中:
if (props.text.length > 0) {
setDefaultDbValue(props.text[0]);
} else {
let defaultDropDownOption:string = "No Db Available";
setDefaultDbValue(defaultDropDownOption);
}
因此,无论哪种方式,您都将执行 setDefaultDbValue()
,这将更新状态并触发重新渲染。
为了防止这个循环,你可以用 useEffect()
包装这个片段并使用你的 text.length
作为依赖:
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]);
不需要将 defaultValue
作为依赖项传递给 useEffect
挂钩。还是我误解了你的意图?
除此之外:设置默认值的更好方法是直接在 useState(<DEFAULT_VALUE>)
挂钩中进行。就像你做的那样。为什么这不适合您?
编辑
经过您的编辑,很明显问题在于 Ant 如何使用道具 defaultValue
。这个道具不是反应性的 - 所以在 Select
渲染后更新它没有效果。
因此您可以在呈现面板之前等待请求解决,或者:
如果你想 select 你的 text
道具的第一个元素,一旦它被加载,你必须相应地设置 Select
的 value
。 注意如果将值绑定到状态变量,则必须通过 onChange
处理程序更新值。
举个例子:
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"/>
我是 React 和学习 hooks 的新手,但我无法在第二次执行时摆脱 useState。
我的调用函数正在调用另一个名为 QueryPanel 的组件,参数为:
const [availables, setAvailable] = useState<string[]>([]);
useEffect(() => {
context.services.records.getAvailablesList()
.then((resp) => {
if(resp != undefined) {
setAvailables(resp);
}
});
}, []);
return <Panel1 text={availables}></Panel1>;
所以 Panel1 以字符串数组或空数组打开。我想要做的是显示下拉列表,如果参数不为空,则将第一项显示为默认项,或者如果参数列表为空,则将字符串显示为默认值“无可用选项”
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);
// }
我的 if-else 陷入了无限循环,因为每当我将默认值设置为一个值时,它就会再次进入相同的 if 语句。如何在不使用额外字段的情况下摆脱它? useEffect 在这里被注释掉了,奇怪的是当我检查参数 prop.text 它在开始时是 undefined 这就是代码的原因不会在 useEffect 中进入 if-else,然后在第二次迭代中 prop.text 变为 true。
我尝试过使用 useEffect 钩子但没有成功:
useEffect(() => {
if (props.text.length > 0) {
setDefaultValue(props.text[0]);
}
}, [defaultValue]);
但是这次默认数据库变空了
这里是Panel1(QueryPanel)的完整代码,只是有些名字不同
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={"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={"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={"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={"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={"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={"inline-form-item"}>
<Button type="primary" htmlType="submit">
{t.trans.queryPanel.search}
</Button>
</Form.Item>
</Col>
</Row>
</Form>
</div>
);
}
我认为一种方法是:
useEffect(() => {
if (props.text.length > 0 && props.text[0] !== defaultValue) {
setDefaultValue(props.text[0]);
}
}, [defaultValue]);
这样,您将仅在 defaultValue
不同于 props.text[0]
创建无限循环是因为只要组件状态发生变化,React 就会重新渲染您的 <Panel1 />
组件。
在您的组件主体中,您有这个 if ... else
语句,但没有将其包裹在钩子中:
if (props.text.length > 0) {
setDefaultDbValue(props.text[0]);
} else {
let defaultDropDownOption:string = "No Db Available";
setDefaultDbValue(defaultDropDownOption);
}
因此,无论哪种方式,您都将执行 setDefaultDbValue()
,这将更新状态并触发重新渲染。
为了防止这个循环,你可以用 useEffect()
包装这个片段并使用你的 text.length
作为依赖:
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]);
不需要将 defaultValue
作为依赖项传递给 useEffect
挂钩。还是我误解了你的意图?
除此之外:设置默认值的更好方法是直接在 useState(<DEFAULT_VALUE>)
挂钩中进行。就像你做的那样。为什么这不适合您?
编辑
经过您的编辑,很明显问题在于 Ant 如何使用道具 defaultValue
。这个道具不是反应性的 - 所以在 Select
渲染后更新它没有效果。
因此您可以在呈现面板之前等待请求解决,或者:
如果你想 select 你的 text
道具的第一个元素,一旦它被加载,你必须相应地设置 Select
的 value
。 注意如果将值绑定到状态变量,则必须通过 onChange
处理程序更新值。
举个例子:
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"/>