I have kind of an easy problem, but I'm stuck because I do not know TypeScript well (and I need to get to know it very quickly).
I have to add simple validation on submit which will check if a value is not empty.
I have the simplest React form:
type FormValues = {
title: string;
};
function App() {
const [values, handleChange] = useFormState<FormValues>({
title: ""
});
const handleSubmit = (evt: FormEvent) => {
evt.preventDefault();
console.log("SUBMITTED");
};
return (
<div>
<form onSubmit={handleSubmit}>
<input
id="title"
name="title"
onChange={handleChange}
type="text"
value={values.title}
/>
<button type="submit">submit</button>
</form>
</div>
);
}
Custom hook to handle form:
import { useCallback, useReducer } from "react";
type InputChangeEvent = {
target: {
name: string;
value: string;
};
};
function useFormState<T extends Record<string, string>>(initialValue: T) {
const [state, dispatch] = useReducer(
(prevState: T, { name, value }: InputChangeEvent["target"]) => ({
...prevState,
[name]: value
}),
initialValue
);
const handleDispatch = useCallback(
(evt: InputChangeEvent) => {
dispatch({
name: evt.target.name,
value: evt.target.value
});
},
[dispatch]
);
return [state, handleDispatch] as const;
}
export default useFormState;
Now I'd like to add simple validation on submit and (because of TS) I have no idea how. I've thought about three options:
- Put the validation logic to the
handleSubmitmethod. - Put the validation logic inside custom
useFormStatehook. - Create another custom hook just to manage validation only.
I tried to handle the first (and I think the worst) solution in this CodeSandbox example, but as I said the TS types are stronger than me and the code does not work.
Would anyone be so kind and help me with both cases (pick the most correct solution and then, run the code with TS)?
CodePudding user response:
A common approach would be to just store the form error value in a React useState variable and render it as necessary.
const [error, setError] = React.useState('');
// ...
const handleSubmit = (evt: FormEvent) => {
evt.preventDefault();
// This might be the wrong way to check the input value
// I haven't worked directly with form submit events in a hot minute
if (!evt.target.value) {
setError('Title is required');
}
};
// ...
<input
id="title"
name="title"
onChange={handleChange}
type="text"
value={values.title}
aria-describedby="title-error"
required
aria-invalid={!!error}
/>
{error && <strong id="title-error" role="alert">{error}</strong>}
Notice that the aria-describedby, required, aria-invalid and role attributes are important to enforce semantic relationships between the input and its error, announce the error to screen readers when it appears, and designate the input as "required".
If you had multiple inputs in the form, you can make your error value into an object that can store a separate error for each field of your form:
const [errors, setErrors] = React.useState({});
// ...
setErrors(oldErrors => ({...oldErrors, title: "Title is required"}));
// ...
{errors.title && <strong id="title-error" role="alert">{errors.title}
Another common pattern is to clear an input error when it is modified or "touched" to allow the form to be resubmitted:
onChange={(e) => {
setError(''); // or setErrors(({title: _, ...restErrors}) => restErrors);
handleChange(e);
}}
Note that all of this error handling logic can be rolled into your custom hooks for form/input handling in general, but does not have to be.
