🙌

MUIのAutocompleteをreact-hook-form(v7)で使う

2022/06/13に公開

そんなに難しい事無いかと思ってやってみたら、意外と面倒なところが有ったので。

バージョン

  • React 17.0.2
  • react-hook-form 7.32.0
  • yup 0.32.11
  • @mui 5.8.3

実現したいこと

  • Autocompleteの入力値をreact-hook-form(+yup)で制御したい
  • react-hook-formのresetとかsetValueもきちんと動いて欲しい
  • 入力候補以外にも自由入力(freeSoloオプション)を有効に

とりあえずregisterで実装してみる

TextFieldにreact-hook-formを組み込むのと同じノリで、AutoCompleteに組み込み

yupとuseForm

const schema = yup.object().shape({
  pt1: yup.string().required()
});
const {
    control,
    register,
    reset,
    getValues,
    setValue,
    trigger,
    handleSubmit,
    formState: { errors }
  } = useForm({
    resolver: yupResolver(schema),
    shouldUnregister: false,
    defaultValues: {
      pt1: ""
    }
  });

Autocomplete実装

        <Autocomplete
          name="pt1"
          {...register("pt1")}
          freeSolo
          options={options}
          renderInput={(params) => (
            <TextField
              {...params}
              label="Autocomplete with register"
              error={"pt1" in errors}
              helperText={errors.pt1?.message}
            />
          )}
        />

画面上では特に問題ないのですがreact-hook-formの方に値がセットされず、getValuesしてもonSubmitしてもデフォルトの空文字列が戻ってきてしまいます。

AutocompleteのonInputChangeでreact-hook-formのsetValue

なのでAutocompleteのonInputChangeでsetValueを実行して、直接react-hook-formに値をセット
(今回freeSoloオプション付きなのでonChangeだと自由入力の値がセットされないのでonInputChangeで)
一応コレで選択肢と自由入力両方ともgetValuesで取得できるのですが・・・

        <Autocomplete
          name="pt1"
          {...register("pt1")}
          freeSolo
          options={options}
          onInputChange={(e, newValue) => {
            setValue("pt1", newValue);
          }}
          renderInput={(params) => (
            <TextField
              {...params}
              label="Autocomplete with register"
              error={"pt1" in errors}
              helperText={errors.pt1?.message}
            />
          )}
        />

resetの挙動がおかしい?

  • reset()を実行すると画面上は消えるのだけど、submit()したりtrigger()すると変更前の値が表示されてしまう
  • AutocompleteのvalueとinputValueが独立なので、valueだけリセットされてinputValueの方がリセットされてないように思うけど・・・ ( https://mui.com/material-ui/react-autocomplete/#country-select )
  • どうにもここのresetの挙動だけは思った通りに動作できませんでした。

Controllerを使って実装してみる

react-hook-formのControllerを使うパターン

        <Controller
          name="pt1"
          control={control}
          render={({ field, fieldState }) => {
            return (
              <Autocomplete
                {...field}
                freeSolo
                onInputChange={(e, newValue) => {
                  setValue("pt1", newValue);
                }}
                options={options}
                renderInput={(params) => {
                  // console.log(params);
                  return (
                    <TextField
                      {...params}
                      error={fieldState.invalid}
                      helperText={fieldState.error?.message}
                      label="Autocomplete with Controller"
                    />
                  );
                }}
              />
            );
          }}
        />

この記述だと何故か選択肢を選んだ後に「0」になってしまう???
どうもスプレッドで展開しているfield.onChangeが何か想定外の動作をしてるっぽい?

renderのfield.onChangeを無視する

  • fieldの中身は{name, ref, value, onChange, onBlur} なのでonChangeとonBlurは使わない
        <Controller
          name="pt1"
          control={control}
          render={({ field, fieldState, formState }) => {
            return (
              <Autocomplete
                name={field.name}
                ref={field.ref}
                value={field.value}
                freeSolo
                onInputChange={(e, newValue) => {
                  setValue("pt1", newValue);
                }}
                options={options}
                renderInput={(params) => {
                  return (
                    <TextField
                      {...params}
                      error={fieldState.invalid}
                      helperText={fieldState.error?.message}
                      label="Autocomplete with Controller"
                    />
                  );
                }}
              />
            );
          }}
        />
  • 何かスッキリしない気もしますが、一応コレで想定したような動作にはなりました。

(一応)完成形

とりあえず以下のような実装になりました。

import { yupResolver } from "@hookform/resolvers/yup";
import {
  Autocomplete,
  Button,
  TextField,
  Container,
  Stack
} from "@mui/material";
import { useForm, Controller } from "react-hook-form";
import * as yup from "yup";

const schema = yup.object().shape({
  pt1: yup.string().required()
});

const options = [
  "Robert Wyatt",
  "Mike Ratledge",
  "Kevin Ayeres",
  "Hugh Hopper",
  "Elton Dean",
  "Lyn Dobson",
  "Karl Jenkins",
  "Phil Haward",
  "John Marshall"
];

export default function App() {
  const {
    control,
    reset,
    getValues,
    setValue,
    trigger,
    handleSubmit
  } = useForm({
    resolver: yupResolver(schema),
    shouldUnregister: false,
    defaultValues: {
      pt1: ""
    }
  });

  const onSubmit = (data) => {
    console.log("onSubmit");
    console.log(data);
  };

  return (
    <Container maxWidth="sm" sx={{ pt: 5 }}>
      <Stack spacing={2}>
        <Controller
          name="pt1"
          control={control}
          render={({ field, fieldState, formState }) => {
            //1qconsole.log(field);
            return (
              <Autocomplete
                name={field.name}
                ref={field.ref}
                value={field.value}
                freeSolo
                onInputChange={(e, newValue) => {
                  setValue("pt1", newValue);
                }}
                options={options}
                renderInput={(params) => {
                  // console.log(params);
                  return (
                    <TextField
                      {...params}
                      error={fieldState.invalid}
                      helperText={fieldState.error?.message}
                      label="Autocomplete with Controller"
                    />
                  );
                }}
              />
            );
          }}
        />
        <Button
          variant="outlined"
          onClick={() => {
            reset();
          }}
        >
          Reset
        </Button>
        <Button
          variant="outlined"
          onClick={() => {
            console.log(getValues());
          }}
        >
          Get Values
        </Button>
        <Button
          variant="outlined"
          onClick={() => {
            trigger();
          }}
        >
          Trigger All
        </Button>
        <Button
          variant="outlined"
          onClick={() => {
            setValue("pt1", "John Etheridge");
          }}
        >
          SET value
        </Button>
        <Button variant="outlined" onClick={handleSubmit(onSubmit)}>
          Submit
        </Button>
      </Stack>
    </Container>
  );
}
  • react-hook-formのregisterとControllerのrender.fieldってほぼ同等かと思ってたのですけど render.fieldの方はvalueの設定が有るのに今回気づきました。
  • registerの方でもきちんと動作する設定知ってる方いらっしゃいましたら教えてください。
  • 何か・・・間違った使い方してる気がしないでもないです。
  • テスト用にcodesandbox置いておきます。

Discussion