formikの入力フォームを共通化するメモ（しかし中途半端）

# はじめに

自分が作ろうとしていたちょっとした個人用のWebアプリでは、入力画面の使い方がシンプルで、画面数も少なかったことから、工夫すれば入力フォームを共通化できそう（コンポーネントとして切り出せそう）だな、と思っていた。で、実際試したところ、とりあえず動作するものが作れたので、そのメモ。
しかし初めに書いておくと、タイトルにも書いた通り、これは実のところ中途半端で、少しでも動的な要素を画面に求めようとすると破綻する。結果的にこのWebアプリでも採用することなくお蔵入りしたのだが、一応動くものは作れたので備忘録として残す。どの辺が中途半端なのかについては後述する。（なにか色々考慮漏れがあるんだとは思うが…）

# やりたかったこと

自分が作ろうとしていた入力画面は、究極的にいうと
- `yup`でvalidationする
- 入力したフォームの内容をAPIにPOSTする

ができればそれでよかった。ということは、
- 各画面固有の入力項目（formikの`Field`タグ群等）
- 各画面ごとの入力フォームに対応する`yup`のスキーマ
- 入力した値をPOSTする先のAPIリクエストパス

を外から渡してもらってハメ込んで、いい感じに入力フォームとして成立させるコンポーネントが作れるんじゃないかと考えた。

# 入力フォームのコンポーネント

```typescript:/components/input-form.tsx
import React, { ReactNode } from "react";
import axios from "axios";

import * as yup from 'yup';
import { useState } from "react";

import { Formik, Field, Form, ErrorMessage } from "formik";

const InputForm = ( initialData:any , schema:yup.ObjectSchema<any> , apipath:string, indivisualForm:JSX.Element ) => {

    const [status,setStatus] = useState('');
    const submitData = async (data:any) => {
        setStatus('now posting...');

        const res = await axios.post(apipath,data);
        if(res.status == 200) {
            setStatus('succeeded! [reload]');
            window.location.reload();
        } else {
            setStatus('failed:' + res.data.message);
        }
    };
    return (

        <Formik 
            initialValues={initialData}
            validationSchema={schema}
            onSubmit={submitData}
            enableReinitialize
        >
            {(formik)=>{
                return (
                    <Form>
                            <div className="status">
                                <p>{status}</p>
                            </div>
                            {indivisualForm}
                            <div>
                                <button type="submit"  disabled={!formik.isValid || formik.isSubmitting}>submit</button>
                            </div>
                    </Form>    
                )
            }}
        </Formik>
    );
}

export default InputForm;
```

- 引数は4つ。基本は↑に書いた通り。
    -  `initialData` : `Formik` の`initialValues`に渡すパラメータ。新規入力のときは空文字とかnullとか、更新のときは値詰めて渡す感じ。
    -  `schema` : `yup` のスキーマ。Formikの`validationSchema`に渡すパラメータ。詳細は割愛する
    -  `apipath`: 入力フォームをPOSTする先のAPIリクエストパス。`"/api/post-api"`みたいな文字列を渡す。POSTの実行は`submitData`関数内で`axios`でやる。
    -  `indivisualForm`: 各画面固有の入力フォーム群。型は`JSX.Element`。
- ちょっと頑張ったんだがいい感じに型定義ができなかったので`any`型が目立つ。この辺も正直「中途半端」な側面の一つ。`initialValues`と`validationSchema`は頑張ればもっといい感じの型が付けられそうな気はする。追跡できていない。

# 各ページからの呼び出し方

各ページからは以下のように呼び出す。例えば`/pages/post-test`ってページがあった場合を例とする。

```typescript:/pages/post-test.tsx
import React from "react";
import InputForm from '@/components/input-form';
import * as yup from 'yup';

import { Formik, Field, Form, ErrorMessage , useFormikContext , useField } from "formik";
import { useState } from "react";
type InputFieldsType = {
    test: string,
    id: number,
};

const schema = yup.object().shape({
    test: yup.string().min(1, 'min 1 length').max(10, 'max 10 length').required(),
    id: yup.number().positive().integer().required(),
});

const InputFields = () =>  {

    return (
        <div>

                            <div>
                                <label htmlFor="test">test</label>
                                <Field name="test" type="text"
                                 />
                                <ErrorMessage name="test" />
                            </div>
                            <div>
                                <label htmlFor="id">id</label>
                                <Field name="id" type="number" />
                                <ErrorMessage name="id" />
                            </div>
        </div>
    );
};

const Index: React.FC<any> = ( props ,  ) =>{

    return (

            <div>
                <h3>post-test import test</h3>
                {InputForm(
                    {test:'' , id: 0},
                    schema,
                    '/api/post-test',
                    InputFields()
                )
                }
            </div>
    );
}

export default Index;
```

ポイントは
```typescript
                {InputForm(
                    {test:'' , id: 0},
                    schema,
                    '/api/post-test',
                    InputFields()
                )
                }
```
の部分だ。ここで↑の`InputForm`コンポーネントを呼び出している。各画面固有の`Field`タグ群は`InputFields`関数として別に定義されており、これをそのまま引き渡して、入力フォームとして`InputForm`コンポーネント側で`indivisualForm:JSX.Element`というパラメータで受け取り、自分のとこにハメ込んで最終的にFormikの画面として成立させるという流れだ。
各画面の入力項目の配置等、画面毎のレイアウトをページ側で指定したうえで、入力フォームとして成立させるための他の共通的な要素はコンポーネントに一元化できる、ということで、個人的にこの発想自体は気に入っている。実際、少なくともこの例は動く。ビルドも通るし、画面もアクセスできるし、バリデーションも想定通りに動くし、APIへのPOSTも想定通りにリクエストされる。この点だけみれば何も問題はなかった。確認したバージョンは以下。

```json
    "next": "13.2.4",
    "react": "18.2.0",
    "react-dom": "18.2.0",
    "formik": "^2.4.3",
    "yup": "^1.2.0"
```

# できないこと

ただ、各画面に動的な要素、具体的には`setFieldValue`とか使った動きをつけようと思うと、この構成だとできない。よく考えれば当たり前なんだが、`formik`オブジェクト自体が各ページに存在しない（コンポーネント側にしか存在しない）ので、各ページからその辺を指示できない。例えば`/pages/post-test.tsx`に以下のようなコードいれると、画面表示時にエラーになる。(`setFieldValue`なんてモン存在しねェーよ！って怒られる)

```typescript
...(略)...
const InputFields = () =>  {
    const {setFieldValue} = useFormikContext() 
    const onClickSetTest = (e:React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
        setFieldValue('test' , new Date().toISOString());
    }
    return (
        <div>
            <p>
                <button onClick={onClickSetTest}>set test</button>
            </p>
...(略)...
    );
};
```

この例は、画面に一つボタンを用意して、そのボタンを押すと`test`というフィールドに値をセットするという動きを実現したいもの。Formikの場合、項目への値のセットには`setFieldValue`を使うことになるが、`Formik`の外側であるこのページの実装だと、`useFormikContext()`の戻り値が`undefinded`になるため、想定通りに動作しない。

`useState`やら`useRef`やら使えば（仮にこのように入力項目群と入力フォームのコンポーネントが分離されていても）簡単に実現できるシンプルな動きなのだが、Formik使ってる場合のこの構成だと想定通りには動かない。多分Formikの使い方が悪い（というか何か着想を間違っている気がする）のだが、簡単に入力フォームが作れるFormikというライブラリを使う上での弊害かなあと感じた。どうしようもなさそうだし画面数も少ないので、今回はこの構成での入力フォーム作成は諦めたが、発想自体はいいところまでいったのにな～惜しいな～ということで記念に記録として残すものとする。