0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Posted at

はじめに

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

やりたかったこと

自分が作ろうとしていた入力画面は、究極的にいうと

  • yupでvalidationする
  • 入力したフォームの内容をAPIにPOSTする

ができればそれでよかった。ということは、

  • 各画面固有の入力項目(formikのFieldタグ群等)
  • 各画面ごとの入力フォームに対応するyupのスキーマ
  • 入力した値をPOSTする先のAPIリクエストパス

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

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

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

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

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

/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;

ポイントは

                {InputForm(
                    {test:'' , id: 0},
                    schema,
                    '/api/post-test',
                    InputFields()
                )
                }

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

    "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なんてモン存在しねェーよ!って怒られる)

...()...
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というライブラリを使う上での弊害かなあと感じた。どうしようもなさそうだし画面数も少ないので、今回はこの構成での入力フォーム作成は諦めたが、発想自体はいいところまでいったのにな~惜しいな~ということで記念に記録として残すものとする。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?