MUIのout-of-range valueのワーニングを解消する

## はじめに
MUIとFormikを使用してフォームを作成する中で、以下のようなワーニングがコンソールに出力されることがありました。
```console
MUI: You have provided an out-of-range value `4` for the select (name="status") component.
Consider providing a value that matches one of the available options or ''.
The available values are `1`, `2`, `3`. 
Error Component Stack...(以降省略)
```

<img width='80%' src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/251137/ef810e13-9e00-f0ff-cd2b-488f50ab9047.png'>

備忘のため、原因と対策をまとめました。

## 何が悪かったか
端的に言えば、**Selectの初期値として選択肢に存在しない値を設定している**場合にワーニングが発生します。

実際にワーニングが発生するコード例で確認します。

```react:ワーニングが発生するコード例
import { useEffect, useState } from "react";
import { Formik, Form, Field, FieldProps } from "formik";
import { Box, FormControl, InputLabel, Select, MenuItem, Alert, Paper, SelectChangeEvent } from "@mui/material";

interface StatusOption {
  id: number;
  name: string;
}

interface UserData {
  id: number;
  name: string;
  status: number;
}

interface FormValues {
  status: number | "";
}

// 選択肢のマスターデータ
const STATUS_OPTIONS: StatusOption[] = [
  { id: 1, name: "準備中" },
  { id: 2, name: "進行中" },
  { id: 3, name: "完了" },
];

// エラーが発生するコンポーネント
const ProblemExample = () => {
  const [userData, setUserData] = useState<UserData | null>(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        // 実際のAPIコールを模擬
        await new Promise((resolve) => setTimeout(resolve, 1000));

        // 問題のあるデータ: DBに存在する値が選択肢にない
        setUserData({
          id: 1,
          name: "テストユーザー",
          status: 4, // STATUS_OPTIONSには存在しない値
        });
      } catch (error) {
        console.error("Error fetching data:", error);
      }
    };

    fetchData();
  }, []);

  if (!userData) {
    return <Alert severity="error">データの取得に失敗しました</Alert>;
  }

  const initialValues: FormValues = {
    status: userData.status, // 問題の原因：存在しない値を直接設定
  };

  return (
    <Box sx={{ p: 2 }}>
      <Formik
        initialValues={initialValues}
        onSubmit={(values: FormValues): void => {
          console.log(values);
        }}
      >
        <Form>
          <FormControl fullWidth>
            <InputLabel id="problem-status-label">ステータス</InputLabel>
            <Field name="status">
              {({ field }: FieldProps) => (
                <Select
                  {...field}
                  labelId="problem-status-label"
                  label="ステータス"
                  onChange={(event: SelectChangeEvent<number>) => {
                    field.onChange(event);
                  }}
                >
                  {STATUS_OPTIONS.map((option) => (
                    <MenuItem key={option.id} value={option.id}>
                      {option.name}
                    </MenuItem>
                  ))}
                </Select>
              )}
            </Field>
          </FormControl>
        </Form>
      </Formik>
    </Box>
  );
};

// メインコンポーネント
const FormikMuiExample = () => {
  return (
    <Box sx={{ p: 4 }}>
      <Paper elevation={2} sx={{ p: 3, mb: 4 }}>
        <ProblemExample />
      </Paper>
    </Box>
  );
};

export default FormikMuiExample;
```

このコードにより、以下のような画面が生成されます。

<img width='90%' src='https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/251137/4eaa75aa-8886-18ce-1902-3529acad7c25.png'>

初期値には何も選択されていません。

上記のコードのポイントは以下の2箇所です。
```react:ProblemExampleのuseEffect内を抜粋
        // 問題のあるデータ: DBに存在する値が選択肢にない
        setUserData({
          id: 1,
          name: "テストユーザー",
          status: 4, // STATUS_OPTIONSには存在しない値
        });
```
まず、模擬的な処理ですがSelectのマスタデータとして用意されているSTATUS_OPTIONSには存在しない値を取得してきています。

```react:ProblemExampleのreturn直前を抜粋
  const initialValues: FormValues = {
    status: userData.status, // 問題の原因：存在しない値を直接設定
  };
```
次に、その取得したデータをなんのチェックもせずに初期値として設定しています。

これにより、Selectには存在しない値が初期値として設定されることになります。
当然、表示すべき値がないため、何も表示されないという状態になります。

## どうすればいいのか
取得してきた値が正しいものかどうかをチェックします。
不正な値の場合のアプローチはいくつか考えられますが、ここでは選択肢の一番初めの値をデフォルト表示させるようにしています。

```react:正しいコード例
// importやinterface定義はワーニングのコード例と変わらないため省略

const CorrectExample = () => {
  const [userData, setUserData] = useState<UserData | null>(null);

  useEffect(() => {
    const fetchData = async () => {
      try {
        await new Promise((resolve) => setTimeout(resolve, 1000));

        setUserData({
          id: 1,
          name: "テストユーザー",
          status: 4, // 存在しない値
        });
      } catch (error) {
        console.error("Error fetching data:", error);
      }
    };

    fetchData();
  }, []);

  if (!userData) {
    return <Alert severity="error">データの取得に失敗しました</Alert>;
  }

  const getValidStatus = (status: number) => {
    return STATUS_OPTIONS.some((option) => option.id === status)
      ? status
      : STATUS_OPTIONS[0].id; // 無効な値の場合は最初の選択肢をデフォルトとする
  };

  const initialValues: FormValues = {
    status: getValidStatus(userData.status),
  };

  return (
    <Box sx={{ p: 2 }}>
      <Formik
        initialValues={initialValues}
        onSubmit={(values: FormValues): void => {
          console.log(values);
        }}
      >
        <Form>
          <FormControl fullWidth>
            <InputLabel id="correct-status-label">ステータス</InputLabel>
            <Field name="status">
              {({ field }: FieldProps) => (
                <Select
                  {...field}
                  labelId="correct-status-label"
                  label="ステータス"
                  onChange={(event: SelectChangeEvent<number>) => {
                    field.onChange(event);
                  }}
                >
                  {STATUS_OPTIONS.map((option) => (
                    <MenuItem key={option.id} value={option.id}>
                      {option.name}
                    </MenuItem>
                  ))}
                </Select>
              )}
            </Field>
          </FormControl>
        </Form>
      </Formik>
    </Box>
  );
};

```

コードのポイントは以下です。
```react:CorrectExample内の処理を抜粋
  const getValidStatus = (status: number) => {
    return STATUS_OPTIONS.some((option) => option.id === status)
      ? status
      : STATUS_OPTIONS[0].id; // 無効な値の場合は最初の選択肢をデフォルトとする
  };
  const initialValues: FormValues = {
    status: getValidStatus(userData.status) //設定値のチェック
  };
```
some関数を使って、指定された`status`がSelectのマスタデータの配列に存在するかをチェックしています。
存在しない場合は、最初の選択肢を返すようにしています。

これにより、仮にマスタデータに存在しない値が初期値として設定されようとしても、防ぐことができるようになります。

## まとめ
蓋をあければなんてことのない話でしたが、原因を突き止めるのには少し時間がかかってしまいました。

同じようなところでつまづいている人にとって役に立つものになっていればいいなと思います。