如何确保使用 useState() 挂钩的 React 状态已更新?

How to make sure a React state using useState() hook has been updated?

我有一个名为 <BasicForm> 的 class 组件,我用它来构建表单。它处理验证和所有形式 state。它通过 React 上下文为输入(呈现为 BasicFormchildren)提供所有必要的功能(onChangeonSubmit 等)。

它按预期工作。问题是,现在我正在将它转换为使用 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 API reference

Using the effect hook

我完全摆脱了 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() 模式。在那里,我还需要确保在提交之前我获得了当前的、经过验证的状态。

到目前为止,一切正常。