📑

MUI Autocompleteをシンプルにマルチセレクトとして使う(手探り)

2022/06/29に公開

MUIでマルチセレクトが<Select>でいけるのかと思ったら、<Autocomplete>使えって書いてあったのでやってみたが割と手探りだったので記録に残す。
https://mui.com/material-ui/react-autocomplete/#multiple-values

多分こうかなでやってみたところがあるので至らない点は補足いただけると幸いです。

目的

  • フェッチしてきたデータを選択肢にして、マルチセレクトできるインプットフォームをAutocompleteを使って作成する。
  • react-hook-formを使ってバリデーションする。
  • バリデーションライブラリはZod
  • 環境
    • nextjs12
    • MUI V5
    • Zod
    • react-hook-form

1. スキーマ

profileEditForm.ts
import { z } from "zod";

import { ValidationMsg } from "@constants/ValidationMsg";

export const profileEditFormSchema = z.object({
  name: z
    .string({ required_error: ValidationMsg.REQUIRED })
    .min(1, ValidationMsg.REQUIRED),
  email: z
    .string({ required_error: ValidationMsg.REQUIRED })
    .email(ValidationMsg.Email.VALID),
  assignedGroupIds: z
    .array(z.number(), {
      invalid_type_error: ValidationMsg.INVALID_TYPE,
    })
    .min(1),
});

export type ProfileEditForm = z.infer<typeof profileEditFormSchema>;

2. データフェッチ等

edit.tsx
// ~省略
  const {
    user,
    isLoading: isUserLoading,
    isError: isUserError,
    message: userMessage,
  } = useLoginUser();
  const {
    groups,
    isLoading: isGroupLoading,
    isError: isGroupError,
    message: groupMessage,
  } = useMemberGroups();
  const [groupsAsOptions, setOptions] = useState<MemberGroupList>([]);
  useEffect(() => {
    if (groups && !isGroupLoading && !isGroupError) {
      setOptions(groups);
    }
  }, [groups]);
  const {
    handleSubmit,
    formState: { errors },
    setValue,
    getValues,
    control,
  } = useForm<ProfileEditForm>({
    resolver: zodResolver(profileEditFormSchema),
    defaultValues: {
      assignedGroupIds: [],
      name: "",
      email: "",
    },
  });
  const [selectedGroups, setSelectedGroups] = useState<MemberGroupList>([]);

  const { enqueueSnackbar } = useSnackbar();
  const onSubmit = async (data: ProfileEditForm) => {
    try {
      await userService.updateProfile(data);
      enqueueSnackbar(Snackbar.UPDATED.message, {
        variant: Snackbar.UPDATED.type,
      });
    } catch (e) {
      if (Axios.isAxiosError(e)) {
        const { message } = axiosErrorHandler(e);
        enqueueSnackbar(message, { variant: "error" });
      }
    }
  };
  useEffect(() => {
    if (user && !isUserLoading) {
      const assignedGroupIds = user.assigned_subject_groups
        ? user.assigned_subject_groups.map((g) => g.id)
        : [];
      if (!getValues("assignedGroupIds").length) {
        setValue("assignedGroupIds", assignedGroupIds);
        setSelectedGroups(user.assigned_subject_groups);
      }
      if (!getValues("name")) setValue("name", user.name);
      if (!getValues("email")) setValue("email", user.email);
    }
  }, [user]);
// ~省略

やってること

  • ユーザー情報のフェッチ
  • グループタグのフェッチ
  • Autocompleteのバリューをステート管理
  • RHFの初期値をuseEffectで設定

ポイント
触ってみた感じ、Autocompleteが描画に使っているステートと、RHFが扱うフォームデータは独立させられそうな挙動をしていたので、Autocompleteのバリューと、フォームの内部の値は別々に管理してみた。

Autocomplete

<Controller
    render={({ field: { onChange } }) => (
        <Autocomplete
            loading={isGroupLoading}
            loadingText={FormLabel.LOADING}
            multiple
            value={selectedGroups}
            onChange={(event, item) => {
              const ids = item.map((i) => i.id);
              onChange(ids);
              setSelectedGroups(item);
            }}
            options={groupsAsOptions}
            isOptionEqualToValue={(option, v) =>
                option.id === v.id
            }
            getOptionLabel={(option) => option.name}
            filterSelectedOptions
            renderInput={(params) => (
                <TextField
                    {...params}
                    label={FormLabel.ASSIGNED_SUBJECT_GROUP}
                    error={"assignedGroupIds" in errors}
                    helperText={
                      // @ts-ignore
                      errors.assignedGroupIds?.message
                    }
                />
            )}
        />
    )}
    name="assignedGroupIds"
    control={control}
/>

RHFのコントローラでAutocompleteを制御。
AutocompleteのValueはオプションに入ってくるオブジェクトとして管理して、RHFのフォームの値としてはオブジェクトのIDだけ管理する。その他の部分はほぼ公式ドキュメント準拠。

Autocomplete内部のTextFieldのところで@ts-ignoreしてるのは、下記の型エラーが発生するため。
TS2339: Property 'message' does not exist on type 'FieldError[]'.
実際はFieldErrorが帰ってきて、リストにはならないんだけれど、これは技術的に制約があるからしょうがないんじゃ的なIssueを見たので見つかれば記載する。

一応上記の実装で下記条件は満たした。

  • 非同期で取得したデータをフォームに設定する
  • Autocomplete経由で選択された値は、idのみ扱う

より良い実装や詳しい仕組みの理解が進めば追記する。

Discussion