Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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. It handles validation and all the form state. It provides all the necessary functions (onChange, onSubmit, etc) to the inputs (rendered as children of BasicForm) via React context.

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:

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. So if the user hasn't touched any, BasicForm needs to make sure to 'touch' every input before calling the validateAllFields() function.

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. And that made sure that validateAllField() only got called after the state update (the one that touches all fields).

But when I try to use that second callback parameter with state hooks useState(), I get this error:

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

So, according to the error message above, I'm trying to do this with the useEffect() hook. 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. 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.

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. 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. Thus, I created the submitted state variable so I can know when I need that effect.

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. How do I know that on the next render my submitted state will be already 'false' 100% sure?

Can I rely on React performing every queued state update before the next render? Is this guaranteed?

like image 603
cbdeveloper Avatar asked Mar 25 '19 19:03

cbdeveloper


People also ask

Does React useState hook update immediately?

React do not update immediately, although it seems immediate at first glance.

How do I get updated state in React Hooks?

To update the state, call the state updater function with the new state setState(newState) . Alternatively, if you need to update the state based on the previous state, supply a callback function setState(prevState => newState) .

Why is my state not updating React Hooks?

If you find that useState / setState are not updating immediately, the answer is simple: they're just queues. React useState and setState don't make changes directly to the state object; they create queues to optimize performance, which is why the changes don't update immediately.

Does useState overwrite?

setState in a class component, the function returned by useState does not automatically merge update objects, it replaces them. Try it here, you'll see how the id property is lost. The ... prevState part will get all of the properties of the object and the message: val part will overwrite the message property.


1 Answers

I encountered something like this recently (SO question here), and it seems like what you've come up with is a decent approach.

You can add an arg to useEffect() that should do what you want:

e.g.

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

to watch for changes in 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().

like image 154
Colin Ricardo Avatar answered Sep 20 '22 10:09

Colin Ricardo