React: react-hook-fom with MUI (Material UI)

Here introduces basic code samples of react-hook-form with Material UI, or MUI.

Libraries used here are as follows;

TypeScript: 4.8.2
react: 18.2.0
react-hook-form: 7.35.0
MUI: 5.10.4

Create project with react-create-app with MUI

Create project with typescript template

yarn create react-app my-app --template typescript

Install MUI

yarn add @mui/material @emotion/react @emotion/styled

MUI design depends on the Roboto font. So, please install it, and import related css files in the root index.tsx.

yarn add @fontsource/roboto

src/index.tsx

import '@fontsource/roboto/300.css';
import '@fontsource/roboto/400.css';
import '@fontsource/roboto/500.css';
import '@fontsource/roboto/700.css';

Install react-hook-form

yarn add react-hook-form

If you need datepicker funcitionarity in your form, please install @mui/x-date-pickers and dayjs as well.

yarn add @mui/x-date-pickers dayjs

OK, then. We are ready for creating input form.

Form

This is the final code set including input validation, and localization.

import React, { useEffect, useState } from 'react';
import { useForm, Controller } from 'react-hook-form';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import { DatePicker } from '@mui/x-date-pickers/DatePicker';
import { LocalizationProvider } from '@mui/x-date-pickers';
import dayjs, { Dayjs } from 'dayjs';
// Supported locales: https://github.com/iamkun/dayjs/tree/dev/src/locale
import 'dayjs/locale/ja';
import {
  Box,
  Button,
  Container,
  FormControl,
  FormHelperText,
  InputLabel,
  MenuItem,
  Select,
  Stack,
  TextField,
  Typography,
} from '@mui/material';

type FormFields = {
  name: string;
  description: string;
  priority: number;
  deadline: Dayjs;
};

// Initail form values
const defaultValues: FormFields = {
  name: '',
  description: '',
  priority: 1,
  deadline: dayjs(new Date()),
};

// Validation rules
const validationRules = {
  name: {
    required: 'Please input todo name',
    maxLength: { value: 30, message: 'Name must be 30 characters or less' },
  },
  description: {
    maxLength: { value: 1000, message: 'Description must be 1000 characters or less' },
  },
  priority: {
    required: 'Please select a priority',
  },
  deadline: {
    validate: (val: Dayjs | null) => {
      if (val === null) {
        return 'Please input the deadline';
      }
      if (!val.format('YYYYMMDD').match(/^\d{8}$/g)) {
        return 'Invalid date format';
      }
      return true;
    },
  },
};

// Priority list
const priorityList = [
  { value: 1, label: 'high' },
  { value: 2, label: 'middle' },
  { value: 3, label: 'low' },
];

export default function CreateTodo() {
  const { control, handleSubmit } = useForm<FormFields>({
    defaultValues,
    mode: 'onChange',  // The `onChange` might cause perf issue...
  });

  const onSubmitForm = (values: FormFields) => {
    // Create a todo
  };

  return (
    <Container maxWidth="sm" sx={{ py: 6 }}>
      <Typography variant="h5" sx={{ mb: 3 }}>
        TODO List
      </Typography>

      <Stack component="form" noValidate onSubmit={handleSubmit(onSubmitForm)} spacing={3}>
        {/* Name */}
        <Controller
          name="name"
          control={control}
          rules={validationRules.name}
          render={({ field, fieldState }) => (
            <TextField
              {...field}
              fullWidth
              type="text"
              label="Name"
              error={!!fieldState.error?.message}
              helperText={fieldState.error?.message}
            />
          )}
        />
        {/* Description */}
        <Controller
          name="description"
          control={control}
          rules={validationRules.description}
          render={({ field, fieldState }) => (
            <TextField
              {...field}
              type="text"
              label="description"
              error={!!fieldState.error?.message}
              helperText={fieldState.error?.message}
              multiline
              rows={4}
            />
          )}
        />
        {/* Priority */}
        <Controller
          name="priority"
          control={control}
          rules={validationRules.priority}
          render={({ field, fieldState }) => (
            <FormControl fullWidth error={!!fieldState.error?.message}>
              <InputLabel id="area-label">Priority</InputLabel>
              <Select {...field} labelId="area-label" label="Priority">
                {priorityList.map(({ label, value }) => (
                  <MenuItem key={value} value={value}>
                    {label}
                  </MenuItem>
                ))}
              </Select>
              <FormHelperText>{fieldState.error?.message}</FormHelperText>
            </FormControl>
          )}
        />
        {/* Deadline */}
        <Controller
          name="deadline"
          control={control}
          rules={validationRules.deadline}
          render={({ field, fieldState }) => (
            <LocalizationProvider dateAdapter={AdapterDayjs} adapterLocale="ja">
              <DatePicker
                {...field}
                label="Deadline"
                inputFormat="YYYY/MM/DD"
                renderInput={(params: any) => (
                  <TextField
                    {...params}
                    error={!!fieldState.error?.message}
                    helperText={fieldState.error?.message}
                  />
                )}
                // Validation is not fired with the default react-hook-form mode. So you need this custom onChange event handling.
                onChange={(date) => field.onChange(date)} 
              />
            </LocalizationProvider>
          )}
        />

        <Box sx={{ display: 'flex', justifyContent: 'center', gap: 4 }}>
          <Button type="submit" color="primary" variant="contained">
            Submit
          </Button>
        </Box>
      </Stack>
    </Container>
  );
}