What I'm looking for is to have my custom input validation to show errors input by input, as I type into them (not on submit). What it does now is showing every errors as soon as I type inside any input. I know the reason : because setErrors() is in handleFormChange(), therefore every errors are set as soon as I type in any input, but I can't find a solution that meets my needs.
What I try to do is put most of the form's logic in the Form component, as it is where FormContext is defined. I believe doing so will give me more reusability, as the only parts I would need to change to reuse the components are only the validate function (to define the input's rules) and the onSubmit function, that are located in the parent component (here App). No need to touch anything on Form or FormInput when everything will be set.
I know react-hook-form and how to use it but I would like to create my own form / input validation logic. Everything works as needed expect this error display problem.
Here is the Form component (including FormContext) :
export const FormContext = createContext({
form: {},
errors: {},
});
export default function Form({
formInitialValues,
onSubmit = () => {},
validate = () => {},
children
}) {
const [form, setForm] = useState(formInitialValues);
const [errors, setErrors] = useState({});
const handleFormChange = (e) => {
const { name, value } = e.target || e;
const updatedForm = {
...form,
[name]: value,
};
const errors = validate(updatedForm);
setForm(updatedForm);
setErrors(errors);
};
return (
<FormComp>
<FormContext.Provider value={{
form,
errors,
handleFormChange,
}}>
{ children }
</FormContext.Provider>
<button type="button" onClick={() => onSubmit(form)}>
Envoyer
</button>
</FormComp>
)
}
Here is my FormInput component :
export default function FormInput({
label,
type = 'text',
name,
forwardRef,
}) {
const formContext = useContext(FormContext);
const { form, errors, handleFormChange } = formContext;
const inputRef = useRef(null);
return (
<InputGroup>
<label>{ label }</label>
<input
type={type}
name={name}
value={form[name]}
onChange={handleFormChange}
ref={forwardRef ? forwardRef : inputRef} />
{ errors && errors[name] && errors[name].length > 0 && <span>{ errors[name] }</span> }
</InputGroup>
)
}
And here is my App component, where I define the validate function (that is used in Form -> handleFormChange()) :
function App() {
const initialValues = {
lastName: '',
firstName: '',
};
const validate = (form) => {
let errors = {};
if (form.lastName === '') {
errors.lastName = 'Last name is required'
}
if (form.firstName === '') {
errors.firstName = 'This field is required';
}
return errors;
};
const onSubmit = (form) => {
console.log({ form });
};
return (
<div className="App">
<h1>S'inscrire</h1>
<Form
formInitialValues={initialValues}
onSubmit={onSubmit}
validate={validate}
>
<FormInput
label="Nom"
name="lastName" />
<FormInput
label="Prénom"
name="firstName" />
</Form>
</div>
);
}
export default App;
Could anyone point me in the right direction ? I tried lots of stuff but nothing works (at least nothing that meets my needs). This should be simple but I don't understand why I can't get it right (even though I know where the problem lies). Thank you very much for your help.
---------- EDIT ----------
Ok so I found a way to make it happen, but it seems a little bit "too much" (and still not working properly), I'm guessing there should be a more simple way to achieve what I want.
So what I did is setting a new state errors in the parent component (App) that I use to set the errors in the validate function (they are not set in Form -> handleFormChange() anymore), that I then pass down as a prop to Form. Here are the modified parts :
App component :
function App() {
//.... Cut down unnecessary parts ....
const [errors, setErrors] = useState({});
const validate = (name, value) => {
const omit = (key, { [key]: _, ...obj }) => obj
switch (name) {
case 'lastName':
if (value === '') {
setErrors({
...errors,
[name]: 'Last name required',
});
}
else {
let newObj = omit('lastName', errors);
setErrors(newObj);
}
break;
case 'firstName':
if (value === '') {
setErrors({
...errors,
[name]: 'First name required',
});
}
else {
let newObj = omit('firstName', errors);
setErrors(newObj);
}
break;
}
//.... Same as before ....
}
Then here is the new Form component (I'm just showing the handleFormChange function, everything is the same expect for the fact that I pass down the errors prop from App to Form (used in handleFormChange() -> validate()) :
const handleFormChange = (e) => {
const { name, value } = e.target || e;
const updatedForm = {
...form,
[name]: value,
};
setForm(updatedForm);
validate(name, value);
};
So as you can see, validate() still runs in Form but the errors are not set in Form anymore (they are set inside validate(), in the parent component App, in the switch statement).
This almost works as I would like : the error messages show input by input, but only one input at the time. What I mean is, if I type in and blank the first input, it displays the error for this input only, but then if I type in the second input while the first error is set, the first error message disappears but the second input error message displays (and even if I don't trigger the second input's error, it still hides the first error message), and vice versa.
What am I doing wrong ? I would greatly appreciate some advice. Thank you very much