DEV Community

meijin
meijin

Posted on • Updated on

Implement a Layered Architecture by React Hook Form (v7)

I will talk about the idea of component design using React Hook Form (v7).

React Hook Form

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>}
    </>
  );
};
Enter fullscreen mode Exit fullscreen mode

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}`}
    />
  );
};
Enter fullscreen mode Exit fullscreen mode

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,
  });
Enter fullscreen mode Exit fullscreen mode

And pass control to the RhfTextArea component.

    <RhfTextArea placeholder="post content" name="body" control={control} />
Enter fullscreen mode Exit fullscreen mode

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 });
Enter fullscreen mode Exit fullscreen mode

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>;
};
Enter fullscreen mode Exit fullscreen mode

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";
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

This separation of library dependencies by hierarchy reduces the number of places to look for large updates or library migrations in the future.

References


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.

Discussion (0)