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?
React do not update immediately, although it seems immediate at first glance.
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) .
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.
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.
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()
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With