如何确保使用 useState() 挂钩的 React 状态已更新?
How to make sure a React state using useState() hook has been updated?
我有一个名为 <BasicForm>
的 class 组件,我用它来构建表单。它处理验证和所有形式 state
。它通过 React 上下文为输入(呈现为 BasicForm
的 children
)提供所有必要的功能(onChange
、onSubmit
等)。
它按预期工作。问题是,现在我正在将它转换为使用 React Hooks,我在尝试复制我在 class:
时所做的以下行为时有疑问
class BasicForm extends React.Component {
...other code...
touchAllInputsValidateAndSubmit() {
// CREATE DEEP COPY OF THE STATE'S INPUTS OBJECT
let inputs = {};
for (let inputName in this.state.inputs) {
inputs = Object.assign(inputs, {[inputName]:{...this.state.inputs[inputName]}});
}
// TOUCH ALL INPUTS
for (let inputName in inputs) {
inputs[inputName].touched = true;
}
// UPDATE STATE AND CALL VALIDATION
this.setState({
inputs
}, () => this.validateAllFields()); // <---- SECOND CALLBACK ARGUMENT
}
... more code ...
}
当用户单击提交按钮时,BasicForm
应该 'touch' 所有输入,然后才调用 validateAllFields()
,因为验证错误只会在触摸输入时显示。因此,如果用户没有触摸任何内容,BasicForm
需要确保在调用 validateAllFields()
函数之前 'touch' 每次输入。
当我使用 classes 时,我这样做的方式是在 setState()
函数上使用第二个回调参数,正如您从上面的代码中看到的那样。这确保 validateAllField()
仅在状态更新(涉及所有字段的那个)之后被调用。
但是当我尝试将第二个回调参数与状态挂钩 useState()
一起使用时,我收到此错误:
const [inputs, setInputs] = useState({});
... some other code ...
setInputs(auxInputs, () => console.log('Inputs updated!'));
Warning: State updates from the useState() and useReducer() Hooks
don't support the second callback argument. To execute a side effect
after rendering, declare it in the component body with useEffect().
因此,根据上面的错误消息,我正在尝试使用 useEffect()
挂钩来执行此操作。但这让我有点困惑,因为据我所知, useEffect()
不是基于状态更新,而是在渲染执行中。它在每次渲染后执行。而且我知道 React 可以在重新渲染之前对一些状态更新进行排队,所以我觉得我无法完全控制我的 useEffect()
挂钩何时执行,就像我在使用 [=65= 时所做的那样]es 和 setState()
第二个回调参数。
到目前为止我得到的是(它似乎正在工作):
function BasicForm(props) {
const [inputs, setInputs] = useState({});
const [submitted, setSubmitted] = useState(false);
... other code ...
function touchAllInputsValidateAndSubmit() {
const shouldSubmit = true;
// CREATE DEEP COPY OF THE STATE'S INPUTS OBJECT
let auxInputs = {};
for (let inputName in inputs) {
auxInputs = Object.assign(auxInputs, {[inputName]:{...inputs[inputName]}});
}
// TOUCH ALL INPUTS
for (let inputName in auxInputs) {
auxInputs[inputName].touched = true;
}
// UPDATE STATE
setInputs(auxInputs);
setSubmitted(true);
}
// EFFECT HOOK TO CALL VALIDATE ALL WHEN SUBMITTED = 'TRUE'
useEffect(() => {
if (submitted) {
validateAllFields();
}
setSubmitted(false);
});
... some more code ...
}
我正在使用 useEffect()
挂钩来调用 validateAllFields()
函数。由于 useEffect()
在 每个渲染器上执行 我需要一种方法来知道何时调用 validateAllFields()
因为我不想在每个渲染器上都使用它。因此,我创建了 submitted
状态变量,这样我就可以知道何时需要该效果。
这是一个好的解决方案吗?您可能会想到哪些其他可能的解决方案?就是感觉很奇怪。
假设 validateAllFields()
是一个在任何情况下都不能被调用两次的函数。我怎么知道在下一次渲染时我的 submitted
状态已经 'false' 100% 确定?
我可以依靠 React 在下一次渲染之前执行每个排队的状态更新吗?这个有保障吗?
我最近遇到了这样的事情(SO question ),看来你想出的是一个不错的方法。
您可以向 useEffect()
添加一个 arg 来满足您的需求:
例如
useEffect(() => { ... }, [submitted])
观察 submitted
的变化。
另一种方法是修改挂钩以使用回调,例如:
import React, { useState, useCallback } from 'react';
const useStateful = initial => {
const [value, setValue] = useState(initial);
return {
value,
setValue
};
};
const useSetState = initialValue => {
const { value, setValue } = useStateful(initialValue);
return {
setState: useCallback(v => {
return setValue(oldValue => ({
...oldValue,
...(typeof v === 'function' ? v(oldValue) : v)
}));
}, []),
state: value
};
};
通过这种方式,您可以模拟 'classic' setState()
的行为。
我尝试使用 useEffect()
钩子来解决它,但它并没有完全解决我的问题。它有点工作,但我最终发现它对于这样一个简单的任务来说有点太复杂了,而且我也不太确定我的函数被执行了多少次,以及它是否在状态更改后执行不是。
useEffect()
上的文档提到了效果挂钩的一些用例,其中 none 是我尝试做的用途。
我完全摆脱了 useEffect()
挂钩,并利用了 setState((prevState) => {...})
函数的功能形式,确保您在使用它时会获得状态的当前版本那。于是代码序列变成了下面这样:
// ==========================================================================
// FUNCTION TO HANDLE ON SUBMIT
// ==========================================================================
function onSubmit(event){
event.preventDefault();
touchAllInputsValidateAndSubmit();
return;
}
// ==========================================================================
// FUNCTION TO TOUCH ALL INPUTS WHEN BEGIN SUBMITING
// ==========================================================================
function touchAllInputsValidateAndSubmit() {
let auxInputs = {};
const shouldSubmit = true;
setInputs((prevState) => {
// CREATE DEEP COPY OF THE STATE'S INPUTS OBJECT
for (let inputName in prevState) {
auxInputs = Object.assign(auxInputs, {[inputName]:{...prevState[inputName]}});
}
// TOUCH ALL INPUTS
for (let inputName in auxInputs) {
auxInputs[inputName].touched = true;
}
return({
...auxInputs
});
});
validateAllFields(shouldSubmit);
}
// ==========================================================================
// FUNCTION TO VALIDATE ALL INPUT FIELDS
// ==========================================================================
function validateAllFields(shouldSubmit = false) {
// CREATE DEEP COPY OF THE STATE'S INPUTS OBJECT
let auxInputs = {};
setInputs((prevState) => {
// CREATE DEEP COPY OF THE STATE'S INPUTS OBJECT
for (let inputName in prevState) {
auxInputs =
Object.assign(auxInputs, {[inputName]:{...prevState[inputName]}});
}
// ... all the validation code goes here
return auxInputs; // RETURNS THE UPDATED STATE
}); // END OF SETINPUTS
if (shouldSubmit) {
checkValidationAndSubmit();
}
}
从 validationAllFields()
声明中可以看出,我正在 setInputs( (prevState) => {...})
的调用中执行该函数的所有代码,这确保我将处理更新的当前版本我的 inputs
状态,即:我确定所有输入都已被 touchAllInputsValidateAndSubmit()
触及,因为我在具有函数参数形式的 setInputs()
中。
// ==========================================================================
// FUNCTION TO CHECK VALIDATION BEFORE CALLING SUBMITACTION
// ==========================================================================
function checkValidationAndSubmit() {
let valid = true;
// THIS IS JUST TO MAKE SURE IT GETS THE MOST RECENT STATE VERSION
setInputs((prevState) => {
for (let inputName in prevState) {
if (inputs[inputName].valid === false) {
valid = false;
}
}
if (valid) {
props.submitAction(prevState);
}
return prevState;
});
}
看到我在 checkValidationAndSubmit()
函数中使用了与函数参数调用相同的 setState()
模式。在那里,我还需要确保在提交之前我获得了当前的、经过验证的状态。
到目前为止,一切正常。
我有一个名为 <BasicForm>
的 class 组件,我用它来构建表单。它处理验证和所有形式 state
。它通过 React 上下文为输入(呈现为 BasicForm
的 children
)提供所有必要的功能(onChange
、onSubmit
等)。
它按预期工作。问题是,现在我正在将它转换为使用 React Hooks,我在尝试复制我在 class:
时所做的以下行为时有疑问class BasicForm extends React.Component {
...other code...
touchAllInputsValidateAndSubmit() {
// CREATE DEEP COPY OF THE STATE'S INPUTS OBJECT
let inputs = {};
for (let inputName in this.state.inputs) {
inputs = Object.assign(inputs, {[inputName]:{...this.state.inputs[inputName]}});
}
// TOUCH ALL INPUTS
for (let inputName in inputs) {
inputs[inputName].touched = true;
}
// UPDATE STATE AND CALL VALIDATION
this.setState({
inputs
}, () => this.validateAllFields()); // <---- SECOND CALLBACK ARGUMENT
}
... more code ...
}
当用户单击提交按钮时,BasicForm
应该 'touch' 所有输入,然后才调用 validateAllFields()
,因为验证错误只会在触摸输入时显示。因此,如果用户没有触摸任何内容,BasicForm
需要确保在调用 validateAllFields()
函数之前 'touch' 每次输入。
当我使用 classes 时,我这样做的方式是在 setState()
函数上使用第二个回调参数,正如您从上面的代码中看到的那样。这确保 validateAllField()
仅在状态更新(涉及所有字段的那个)之后被调用。
但是当我尝试将第二个回调参数与状态挂钩 useState()
一起使用时,我收到此错误:
const [inputs, setInputs] = useState({});
... some other code ...
setInputs(auxInputs, () => console.log('Inputs updated!'));
Warning: State updates from the useState() and useReducer() Hooks don't support the second callback argument. To execute a side effect after rendering, declare it in the component body with useEffect().
因此,根据上面的错误消息,我正在尝试使用 useEffect()
挂钩来执行此操作。但这让我有点困惑,因为据我所知, useEffect()
不是基于状态更新,而是在渲染执行中。它在每次渲染后执行。而且我知道 React 可以在重新渲染之前对一些状态更新进行排队,所以我觉得我无法完全控制我的 useEffect()
挂钩何时执行,就像我在使用 [=65= 时所做的那样]es 和 setState()
第二个回调参数。
到目前为止我得到的是(它似乎正在工作):
function BasicForm(props) {
const [inputs, setInputs] = useState({});
const [submitted, setSubmitted] = useState(false);
... other code ...
function touchAllInputsValidateAndSubmit() {
const shouldSubmit = true;
// CREATE DEEP COPY OF THE STATE'S INPUTS OBJECT
let auxInputs = {};
for (let inputName in inputs) {
auxInputs = Object.assign(auxInputs, {[inputName]:{...inputs[inputName]}});
}
// TOUCH ALL INPUTS
for (let inputName in auxInputs) {
auxInputs[inputName].touched = true;
}
// UPDATE STATE
setInputs(auxInputs);
setSubmitted(true);
}
// EFFECT HOOK TO CALL VALIDATE ALL WHEN SUBMITTED = 'TRUE'
useEffect(() => {
if (submitted) {
validateAllFields();
}
setSubmitted(false);
});
... some more code ...
}
我正在使用 useEffect()
挂钩来调用 validateAllFields()
函数。由于 useEffect()
在 每个渲染器上执行 我需要一种方法来知道何时调用 validateAllFields()
因为我不想在每个渲染器上都使用它。因此,我创建了 submitted
状态变量,这样我就可以知道何时需要该效果。
这是一个好的解决方案吗?您可能会想到哪些其他可能的解决方案?就是感觉很奇怪。
假设 validateAllFields()
是一个在任何情况下都不能被调用两次的函数。我怎么知道在下一次渲染时我的 submitted
状态已经 'false' 100% 确定?
我可以依靠 React 在下一次渲染之前执行每个排队的状态更新吗?这个有保障吗?
我最近遇到了这样的事情(SO question
您可以向 useEffect()
添加一个 arg 来满足您的需求:
例如
useEffect(() => { ... }, [submitted])
观察 submitted
的变化。
另一种方法是修改挂钩以使用回调,例如:
import React, { useState, useCallback } from 'react';
const useStateful = initial => {
const [value, setValue] = useState(initial);
return {
value,
setValue
};
};
const useSetState = initialValue => {
const { value, setValue } = useStateful(initialValue);
return {
setState: useCallback(v => {
return setValue(oldValue => ({
...oldValue,
...(typeof v === 'function' ? v(oldValue) : v)
}));
}, []),
state: value
};
};
通过这种方式,您可以模拟 'classic' setState()
的行为。
我尝试使用 useEffect()
钩子来解决它,但它并没有完全解决我的问题。它有点工作,但我最终发现它对于这样一个简单的任务来说有点太复杂了,而且我也不太确定我的函数被执行了多少次,以及它是否在状态更改后执行不是。
useEffect()
上的文档提到了效果挂钩的一些用例,其中 none 是我尝试做的用途。
我完全摆脱了 useEffect()
挂钩,并利用了 setState((prevState) => {...})
函数的功能形式,确保您在使用它时会获得状态的当前版本那。于是代码序列变成了下面这样:
// ==========================================================================
// FUNCTION TO HANDLE ON SUBMIT
// ==========================================================================
function onSubmit(event){
event.preventDefault();
touchAllInputsValidateAndSubmit();
return;
}
// ==========================================================================
// FUNCTION TO TOUCH ALL INPUTS WHEN BEGIN SUBMITING
// ==========================================================================
function touchAllInputsValidateAndSubmit() {
let auxInputs = {};
const shouldSubmit = true;
setInputs((prevState) => {
// CREATE DEEP COPY OF THE STATE'S INPUTS OBJECT
for (let inputName in prevState) {
auxInputs = Object.assign(auxInputs, {[inputName]:{...prevState[inputName]}});
}
// TOUCH ALL INPUTS
for (let inputName in auxInputs) {
auxInputs[inputName].touched = true;
}
return({
...auxInputs
});
});
validateAllFields(shouldSubmit);
}
// ==========================================================================
// FUNCTION TO VALIDATE ALL INPUT FIELDS
// ==========================================================================
function validateAllFields(shouldSubmit = false) {
// CREATE DEEP COPY OF THE STATE'S INPUTS OBJECT
let auxInputs = {};
setInputs((prevState) => {
// CREATE DEEP COPY OF THE STATE'S INPUTS OBJECT
for (let inputName in prevState) {
auxInputs =
Object.assign(auxInputs, {[inputName]:{...prevState[inputName]}});
}
// ... all the validation code goes here
return auxInputs; // RETURNS THE UPDATED STATE
}); // END OF SETINPUTS
if (shouldSubmit) {
checkValidationAndSubmit();
}
}
从 validationAllFields()
声明中可以看出,我正在 setInputs( (prevState) => {...})
的调用中执行该函数的所有代码,这确保我将处理更新的当前版本我的 inputs
状态,即:我确定所有输入都已被 touchAllInputsValidateAndSubmit()
触及,因为我在具有函数参数形式的 setInputs()
中。
// ==========================================================================
// FUNCTION TO CHECK VALIDATION BEFORE CALLING SUBMITACTION
// ==========================================================================
function checkValidationAndSubmit() {
let valid = true;
// THIS IS JUST TO MAKE SURE IT GETS THE MOST RECENT STATE VERSION
setInputs((prevState) => {
for (let inputName in prevState) {
if (inputs[inputName].valid === false) {
valid = false;
}
}
if (valid) {
props.submitAction(prevState);
}
return prevState;
});
}
看到我在 checkValidationAndSubmit()
函数中使用了与函数参数调用相同的 setState()
模式。在那里,我还需要确保在提交之前我获得了当前的、经过验证的状态。
到目前为止,一切正常。