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?
Advertisement
Answer
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()
.