简体   繁体   English

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

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

I had a class component named <BasicForm> that I used to build forms with. 我有一个名为<BasicForm>的类组件,用于构建表单。 It handles validation and all the form state . 它处理验证和所有表单state It provides all the necessary functions ( onChange , onSubmit , etc) to the inputs (rendered as children of BasicForm ) via React context. 它提供了所有必要的功能( onChangeonSubmit等)到输入端(呈现为childrenBasicForm )经由阵营上下文。

It works just as intended. 它按预期工作。 The problem is that now that I'm converting it to use React Hooks, I'm having doubts when trying to replicate the following behavior that I did when it was a class: 问题在于,现在我将其转换为使用React Hooks,在尝试复制以下是类时的行为时,我感到疑惑:

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 ...

}

When the user clicks the submit button, BasicForm should 'touch' all inputs and only then call validateAllFields() , because validation errors will only show if an input has been touched. 当用户单击“提交”按钮时, BasicForm应该“触摸”所有输入,然后才调用validateAllFields() ,因为仅在触摸输入后才会显示验证错误。 So if the user hasn't touched any, BasicForm needs to make sure to 'touch' every input before calling the validateAllFields() function. 因此,如果用户没有触摸任何东西,则BasicForm需要确保在调用validateAllFields()函数之前“触摸”每个输入。

And when I was using classes, the way I did this, was by using the second callback argument on the setState() function as you can see from the code above. 当我使用类时,我这样做的方法是在setState()函数上使用第二个回调参数,正如您从上面的代码中看到的那样。 And that made sure that validateAllField() only got called after the state update (the one that touches all fields). 并确保validateAllField()仅在状态更新(涉及所有字段的状态validateAllField()才被调用。

But when I try to use that second callback parameter with state hooks useState() , I get this error: 但是,当我尝试将第二个回调参数与状态挂钩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. 警告:useState()和useReducer()挂钩的状态更新不支持第二个回调参数。 To execute a side effect after rendering, declare it in the component body with useEffect(). 要在渲染后执行副作用,请使用useEffect()在组件主体中声明它。

So, according to the error message above, I'm trying to do this with the useEffect() hook. 因此,根据上面的错误消息,我正在尝试使用useEffect()挂钩进行此操作。 But this makes me a little bit confused, because as far as I know, useEffect() is not based on state updates, but in render execution. 但这使我有些困惑,因为据我所知, useEffect()并非基于状态更新,而是基于渲染执行。 It executes after every render. 它在每次渲染后执行。 And I know React can queue some state updates before re-rendering, so I feel like I don't have full control of exactly when my useEffect() hook will be executed as I did have when I was using classes and the setState() second callback argument. 而且我知道React可以在重新渲染之前对一些状态更新进行排队,所以我觉得我无法完全控制何时执行useEffect()钩子,就像我在使用类和setState()第二个回调参数。

What I got so far is (it seems to be working): 到目前为止,我得到的是(似乎正在运行):

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 ...

}

I'm using the useEffect() hook to call the validateAllFields() function. 我正在使用useEffect()挂钩来调用validateAllFields()函数。 And since useEffect() is executed on every render I needed a way to know when to call validateAllFields() since I don't want it on every render. 而且由于useEffect()每个渲染器上执行,所以我需要一种方法来知道何时调用validateAllFields()因为我不想在每个渲染器上都调用它。 Thus, I created the submitted state variable so I can know when I need that effect. 因此,我创建了submitted状态变量,以便可以知道何时需要该效果。

Is this a good solution? 这是一个好的解决方案吗? What other possible solutions you might think of? 您可能想到什么其他解决方案? It just feels really weird. 感觉真的很奇怪。

Imagine that validateAllFields() is a function that CANNOT be called twice under no circunstances. 假设validateAllFields()是一个在任何情况下都不能调用两次的函数。 How do I know that on the next render my submitted state will be already 'false' 100% sure? 我怎么知道下一次渲染时,我submitted状态已经100%确定为“假”?

Can I rely on React performing every queued state update before the next render? 我可以依靠React在下一次渲染之前执行每个排队状态更新吗? Is this guaranteed? 这样可以保证吗?

I encountered something like this recently (SO question here ), and it seems like what you've come up with is a decent approach. 最近我遇到了类似的问题( 这里是 SO问题),看来您想出的是一个不错的方法。

You can add an arg to useEffect() that should do what you want: 您可以在useEffect()中添加一个arg,该arg应该可以执行您想要的操作:

eg 例如

useEffect(() => { ... }, [submitted])

to watch for changes in submitted . 观察submitted变化。

Another approach could be to modify hooks to use a callback, something like: 另一种方法是修改挂钩以使用回调,例如:

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
  };
};

In this way you can emulate the behavior of the 'classic' setState() . 这样,您可以模拟“经典” setState()的行为。

I have tried to solve it using the useEffect() hook but it didn't quite solve my problem. 我试图使用useEffect()钩子解决它,但是并不能完全解决我的问题。 It kind of worked, but I ended up finding it a little too complicated for a simple task like that and I also wasn't feeling sure enough about how many times my function was being executed, and if it was being executed after the state change of not. 这种方法行得通,但是我最终发现对于像这样的简单任务而言,它有点太复杂了,而且我对函数执行了多少次以及状态更改后是否执行了函数也不太确定不。

The docs on useEffect() mention some use cases for the effect hook and none of them are the use that I was trying to do. useEffect()上的文档提到了效果挂钩的一些用例,而这些都不是我试图做的用途。

useEffect API reference useEffect API参考

Using the effect hook 使用效果钩

I got rid of the useEffect() hook completely and made use of the functional form of the setState((prevState) => {...}) function that assures that you'll get a current version of your state when you use it like that. 我完全摆脱了useEffect()钩子,并使用了setState((prevState) => {...})函数的功能形式,该函数形式可确保您在使用时获得当前状态的版本像那样。 So the code sequence became the following: 因此,代码序列如下:

  // ==========================================================================
  // 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();
    }

  }

See from the validationAllFields() declaration that I'm performing all my code for that function inside a call of setInputs( (prevState) => {...}) and that makes sure that I'll be working on an updated current version of my inputs state, ie: I'm sure that all inputs have been touched by the touchAllInputsValidateAndSubmit() because I'm inside the setInputs() with the functional argument form. 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;

    });
  }

See that I use that same pattern of the setState() with functional argument call inside the checkValidationAndSubmit() function. 看到我在checkValidationAndSubmit()函数内部使用了带函数参数调用的setState()模式。 In there, I also need to make sure that I'm get the current, validated state before I can submit. 在此之前,我还需要确保已获得当前已验证的状态,然后才能提交。

This is working without issues so far. 到目前为止,这没有任何问题。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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