Implement a Layered Architecture by React Hook Form (v7)
meijin
Posted on September 16, 2021
I will talk about the idea of component design using React Hook Form (v7).
React Hook Form is a library for writing validation logics for forms based on Hooks.
With the separation of the form logic in Hooks, it should be possible to implement the View layer written in TSX and the Logic layer responsible for validation separately.
versions
- React v17
- React Hook Form v7
- Material UI v5
An example of TextArea component
In this section, we will consider the case of implementing a TextArea component.
View layer
First, we will implement a simple component that does not depend on React Hook Form. We also use Material UI as a UI framework.
import { FormHelperText, TextareaAutosize, TextareaAutosizeProps } from '@material-ui/core';
import type { ChangeEventHandler, FocusEventHandler } from "react";
export type TextAreaProps = {
error?: string;
className?: string;
placeholder?: string;
};
export const TextArea = (
props: TextAreaProps & {
inputRef: TextareaAutosizeProps['ref'];
value: string;
onChange: ChangeEventHandler<HTMLTextAreaElement>;
onBlur: FocusEventHandler<HTMLTextAreaElement>;
}
) => {
return (
<>
<TextareaAutosize
minRows={3}
placeholder={props.placeholder}
className={props.className}
ref={props.inputRef}
value={props.value}
onChange={props.onChange}
onBlur={props.onBlur}
/>
{!!props.error && <FormHelperText error>{props.error}</FormHelperText>}
</>
);
};
We have deliberately divided props into TextAreaProps
and non-TextAreaProps, the intention of which will be clarified in the next section.
Logic layer
In the logic layer, we create a separate wrapper component that wraps a simple text area defined as the View layer with the logic of the Form.
import { DeepMap, FieldError, FieldValues, useController, UseControllerProps } from 'react-hook-form';
import { TextArea, TextAreaProps } from '~/components/parts/form/textarea/TextArea';
import formControlStyles from '~/components/parts/form/FormControl.module.scss';
import classNames from 'classnames';
export type RhfTextAreaProps<T extends FieldValues> = TextAreaProps & UseControllerProps<T>;
export const RhfTextArea = <T extends FieldValues>(props: RhfTextAreaProps<T>) => {
const { name, control, placeholder, className } = props;
const {
field: { ref, ...rest },
formState: { errors },
} = useController<T>({ name, control });
return (
<TextArea
inputRef={ref}
className={classNames(formControlStyles.formInput, formControlStyles.formTextArea, className)}
placeholder={placeholder}
{...rest}
error={errors[name] && `${(errors[name] as DeepMap<FieldValues, FieldError>).message}`}
/>
);
};
The component naming is prefixed with Rhf (short for React Hook Form), and the type and other components are dependent on React Hook Form.
The CSS is also imported from a style file dedicated to form controls, named FormControl.module.scss
(*If it receives a className, the parent can change its appearance in any way, which is both good and bad). ).
If you use the useController
hook, you can get the various values needed for the form component, and you can pour them almost directly into the TextArea component.
The TextAreaProps
type is also used for Props in logic layer components. For example, className
is passed from Form and relayed to the bottom View layer. I put these relayed types in TextAreaProps
.
Form layer
Finally, we will show how to actually use the component we created from the form. We'll call this the Form layer.
First, we will get the control variable of the Form from the useForm
hook.
const {
control,
handleSubmit,
setError,
formState: { isValid },
} = useForm<NewPostInput>({
mode: 'onChange',
resolver: yupResolver(newPostSchema),
defaultValues,
});
And pass control
to the RhfTextArea
component.
<RhfTextArea placeholder="post content" name="body" control={control} />
This allows us to do a bit of dependency injection.
On the RhfTextArea
component side, we can take the control
of any form, pass it to useController
, and see the status of that form.
const {
field: { ref, ...rest },
formState: { errors },
} = useController<T>({ name, control });
Since formState
has about 10 properties other than errors, each component can also get the state of the form.
For example, it may be easy to implement disabling a form component when isSubmitting = true
.
export declare type FormState<TFieldValues> = {
isDirty: boolean;
dirtyFields: FieldNamesMarkedBoolean<TFieldValues>;
isSubmitted: boolean;
isSubmitSuccessful: boolean;
submitCount: number;
touchedFields: FieldNamesMarkedBoolean<TFieldValues>;
isSubmitting: boolean;
isValidating: boolean;
isValid: boolean;
errors: FieldErrors<TFieldValues>;
};
Points
Benefits of carving out layers
What are the advantages of separating components in different layers?
The biggest one is that you can use the text area in places other than forms.
It's hard to imagine using a text area in a place other than a form, but for example, a Select box might be used to change the sort order in a list screen. In other words, the implementation of displaying a text area on a form can be divided into two parts: "displaying the text area" and "binding events and styles to it according to the purpose of the form", so that the former can be used more universally.
Another benefit is that it helps to keep dependencies on libraries in order.
If you take a look at the components in the View layer again, you will see that they only depend on Material UI and React:
import { FormHelperText, TextareaAutosize, TextareaAutosizeProps } from '@material-ui/core';
import type { ChangeEventHandler, FocusEventHandler } from "react";
Then, looking at the logic layer, we can see that it depends only on react-hook-form
.
import { DeepMap, FieldError, FieldValues, useController, UseControllerProps } from 'react-hook-form';
import { TextArea, TextAreaProps } from '~/components/parts/form/textarea/TextArea';
import formControlStyles from '~/components/parts/form/FormControl.module.scss';
This separation of library dependencies by hierarchy reduces the number of places to look for large updates or library migrations in the future.
References
- https://koprowski.it/react-native-form-validation-with-react-hook-form-usecontroller/
- https://zenn.dev/erukiti/articles/webform-2021
- https://suzukalight.com/snippet/posts/2021-04-08-react-native-hook-form-yup
For those who have seen this article.
I would be happy to exchange React insights with you, please follow my dev.to account and GitHub account.
Posted on September 16, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.