Open4

react-hook-formとzodとmuiを組み合わせる方法メモ

dashi296dashi296

nameとemailのテキストの入力要素があるフォームはこのようにFormを書きたい

ExampleForm.tsx
import z from "zod";
import { Box, Button } from "@mui/material";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import TextFieldWithController from "../parts/TextFieldWithController";

const schema = z.object({
  name: z.string().trim().min(1),
  email: z.string().email(),
});

export type SchemaType = z.infer<typeof schema>;

const defaultValues: SchemaType = {
  name: "",
  email: "",
};

type Props = {
  onSubmit: (data: SchemaType) => void;
};

export default function ExampleForm({ onSubmit }: Props) {
  const { control, handleSubmit } = useForm({
    resolver: zodResolver(schema),
    defaultValues,
  });

  const onInvalid = (err: unknown) => {
    console.error(err);
  };

  return (
    <Box
      sx={{ display: "flex", flexDirection: "column" }}
      component="form"
      onSubmit={handleSubmit(onSubmit, onInvalid)}
    >
      <TextFieldWithController<SchemaType>
        name="name"
        control={control}
        label="name"
      />
      <TextFieldWithController<SchemaType>
        name="email"
        control={control}
        label="email"
      />
      <Button type="submit">Submit</Button>
    </Box>
  );
}
dashi296dashi296

上記FormのTextFieldWithControllerコンポーネントは下記のように実装する。

TextFieldWithController.tsx
import { TextField, TextFieldProps } from "@mui/material";
import { Controller, Control, FieldValues, FieldPath } from "react-hook-form";

type Props<T extends FieldValues> = TextFieldProps & {
  control: Control<T>;
  name: FieldPath<T>;
};

export default function TextFieldWithController<T extends FieldValues>({
  name,
  control,
  ...textFieldProps
}: Props<T>) {
  return (
    <Controller
      name={name}
      control={control}
      render={({ field, formState: { errors } }) => {
        const error = name in errors;
        const errorText = errors[name]?.message as string;
        return (
          <TextField
            {...textFieldProps}
            {...field}
            error={error}
            helperText={errorText}
          />
        );
      }}
    />
  );
}

別のカスタムしたTextFieldをCustomTextFieldとして下記のように定義し、

CustomTextField.tsx
import { TextField, styled } from "@mui/material";

export default styled(TextField)(() => ({
  "& fieldset": {
    // ここにfieldsetのスタイル
  },
  "& input": {
    // ここにinputのスタイル
  },
  "& label": {
    // ここにlabelのスタイル
  }
}));

form側で下記のようにcomponentプロパティを使ってスタイルを上書きして使うこともできる。

ExampleForm.tsx
      <TextFieldWithController<SchemaType>
+       component={CustomTextField}
        name="name"
        control={control}
        label="name"
      />

よく使うTextFieldのコンポーネントがある場合はTextFieldWithController側でdefault propsとして定義することもできる

TextFieldWithController.tsx
import { TextField, TextFieldProps } from "@mui/material";
import { Controller, Control, FieldValues, FieldPath } from "react-hook-form";
import CustomTextField from "./CustomTextField";
type Props<T extends FieldValues> = TextFieldProps & {
  control: Control<T>;
  name: FieldPath<T>;
};

export default function TextFieldWithController<T extends FieldValues>({
+ component = CustomTextField,
  name,
  control,
  ...textFieldProps
}: Props<T>) {
  return (
    <Controller
      name={name}
      control={control}
      render={({ field, formState: { errors } }) => {
        const error = name in errors;
        const errorText = errors[name]?.message as string;
        return (
          <TextField
+           component={component}
            {...textFieldProps}
            {...field}
            error={error}
            helperText={errorText}
          />
        );
      }}
    />
  );
}

...