【React】React Hook FormのuseFieldArrayを入れ子で使いたい

## 概要
React Hook Formで、配列形式で入力内容を管理したい場合に使用するのが`useFieldArray`になります。
ケースとしてはあまりないかもしれませんが、useFieldArrayの配下で別の配列項目でuseFieldArrayを使用したい場合どう実装するのか、メモ書きします。

## 前提
* 今回使用したReact Hook Formのバージョンは`7.51.4`です。

## 対応方法
[React Hook Form - useFieldArray nested arrays](https://codesandbox.io/p/sandbox/react-hook-form-usefieldarray-nested-arrays-m8w6j?file=%2Fsrc%2FfieldArray.js%3A51%2C16)の実装サンプルにある通り、親配列と子配列を別コンポーネントにする対応が挙げられます。子のコンポーネントでは、親配列のindexを受け取り自身のuseFieldArrayに設定する形になります。

## 実装サンプル
上記の実装サンプルを参考にして、私の方で実装したものが以下になります。

まずはformのschemaです。zodを用いて以下のように定義します。
```react
export const voteResultInputFormSchema = z.object({
  voteRaceList: z.array(
    z.object({
      raceId: z.string(),
      voteRaceContents: z.array(
        z.object({
          contents: z.string().optional(),
        })
      ),
    })
  ),
});
```

以下は親配列を設定するコンポーネントです。
子のコンポーネントに親のindexを渡します。
```react
export const RegisterVoteInputComponent: FC<Props> = ({ raceInfoList }) => {
  const form = useForm<z.infer<typeof voteResultInputFormSchema>>({
    resolver: zodResolver(voteResultInputFormSchema),
    mode: "onSubmit",
    reValidateMode: "onSubmit",
  });
  const control = form.control;
  const {
    fields: voteRaceFields,
    prepend: voteRacePrepend,
    remove: voteRaceRemove,
  } = useFieldArray({
    control,
    name: "voteRaceList",
  });

  return (
    <Form {...form}>
      <form>
        <div className="flex flex-col gap-4 ml-2">
          {voteRaceFields.length < raceInfoList.length && (
            <div>
              <Button
                className={buttonStyle({ color: "lime" })}
                onClick={() => {
                  voteRacePrepend({});
                }}
                type="button"
              >
                レースを追加
              </Button>
            </div>
          )}
          {voteRaceFields.map((race, index) => {
            return (
              <Card key={race.id}>
                <CardContent className="p-2 min-w-[350px]">
                  <div className="flex gap-4">
                    <FormField
                      control={form.control}
                      name={`voteRaceList.${index}.raceId`}
                      render={({ field }) => (
                        <FormItem className="min-w-[200px]">
                          <Select
                            onValueChange={field.onChange}
                            defaultValue={field.value}
                          >
                            <FormControl>
                              <SelectTrigger>
                                <SelectValue placeholder="レースを選択" />
                              </SelectTrigger>
                            </FormControl>
                            <SelectContent>
                              <SelectItem value="">レースを選択</SelectItem>
                              {raceInfoList.map((r) => {
                                return (
                                  <SelectItem key={r.id} value={r.id}>
                                    {r.raceName}
                                  </SelectItem>
                                );
                              })}
                            </SelectContent>
                          </Select>
                          <FormMessage />
                        </FormItem>
                      )}
                    />
                    {voteRaceFields.length > 1 && (
                      <div>
                        <Button
                          className={`${buttonStyle({
                            color: "gray",
                          })} w-18`}
                          onClick={() => {
                            voteRaceRemove(index);
                          }}
                          type="button"
                        >
                          レースを削除
                        </Button>
                      </div>
                    )}
                  </div>
                  <div className="mt-2">
                    {/* 子のコンポーネントにindexを渡す */}
                    <RegisterVoteContentsInputComponent
                      control={form.control}
                      raceIndex={index}
                    />
                  </div>
                </CardContent>
              </Card>
            );
          })}
        </div>
      </form>
    </Form>
  );
};
```

以下は子配列を設定するコンポーネントです。
親から受け取ったindexをuseFieldArrayに設定します。
```react
export const RegisterVoteContentsInputComponent: FC<Props> = ({
  control,
  raceIndex,
}) => {
  // 親から受け取ったindexを設定してuseFieldArrayを使用
  const {
    fields: voteRaceContentFields,
    prepend: voteRaceContentPrepend,
    remove: voteRaceContentRemove,
  } = useFieldArray({
    control,
    name: `voteRaceList.${raceIndex}.voteRaceContents`,
  });

  return (
    <div>
      <Button
        className={`${buttonStyle({ color: "yellow" })} mb-3`}
        onClick={() => {
          voteRaceContentPrepend({});
        }}
        type="button"
      >
        内容追加
      </Button>
      {voteRaceContentFields.map((c, index) => (
        <div className="flex-col gap-3" key={c.id}>
          <FormField
            control={control}
            name={`voteRaceList.${raceIndex}.voteRaceContents.${index}.contents`}
            render={({ field }) => (
              <FormItem>
                <FormLabel>内容</FormLabel>
                <FormControl>
                  <Textarea {...field} className={inputTextAreaStyle()} />
                </FormControl>
                <FormMessage />
              </FormItem>
            )}
          />
          {voteRaceContentFields.length > 1 && (
            <div className="mt-4 mb-2">
              <Button
                className={`${buttonStyle({ color: "gray" })} w-28`}
                onClick={() => {
                  voteRaceContentRemove(index);
                }}
                type="button"
              >
                内容を削除
              </Button>
            </div>
          )}
        </div>
      ))}
    </div>
  );
};
```

## その他参考
* [【React Hook Form】useFieldArrayで付与されるkey属性の値について（2024年3月時点）](https://zenn.dev/someone7140/articles/b8339a51640bc9)