LoginSignup
7
6

More than 3 years have passed since last update.

ReactHooksで複数の汎用フォーム部品を1つのstateとハンドラで管理するをTypeScriptで

Posted at

フォーム部品のstateを一つのstateとハンドラで管理するにあたり、TypeScriptでなるべく型定義してみました。
なお、各HTML要素のインターフェース名はこちらで確認できます。
Web API | MDN

フォーム部品のインターフェース

各フォーム部品が共通して実装するインターフェースの定義。
namevalueonChangeが肝で、後はまあ共通して必要そうなものをいくつか。
それぞれのフォーム部品が扱うデータ型をジェネリック(Generics)で指定します。
テキストボックスはstring、チェックボックスはbooleanなど。

interface FormPartsProps<T> {
  name: string;
  value: T;
  onChange(param: { [name: string]: T }): void;
  label?: string;
  required?: boolean;
  errMsg?: string;
  onError?(param: { [name: string]: string }): void;
}

State管理用Hook

フォーム部品をまとめて管理する用のHooksです。
各フォーム部品から{ name: value }の形式でデータを受け取り、現在のstateにマージさせます。

type HandleChange = (params: { [name: string]: any }) => void;

const useFormState = <T = any>(initialState: T): [T, HandleChange] => {
  const [state, setState] = useState<T>(initialState);

  const handleChange = useCallback<HandleChange>(params => {
    setState(state => ({
      ...state,
      ...params,
    }));
  }, []);

  return [state, handleChange];
};

共通のインターフェースを持つフォーム部品たち

上記のインターフェースFormPartsPropsの実装例。
部品独自に受け取りたいpropsがある場合は、継承して再定義します。
onChange関数へは、共通して{ [name]: [anyValue] }の形式でデータを渡します。
メール形式やパスワード強度など、部品独自のバリデート処理は部品側でやります。

// 性別ラジオボタン
const SelectSex: React.FC<FormPartsProps<string>> = ({
  name,
  value,
  onChange,
}) => {
  const handleChange = useCallback(
    (e: React.ChangeEvent<HTMLInputElement>) =>
      onChange({ [name]: e.target.value }),
    [name, onChange],
  );

  return (
    <div>
      <label>
        <input type="radio"
          name={name}
          value="1"
          checked={value === '1'}
          onChange={handleChange}
        />
        男性
      </label>
      <label>
        <input type="radio"
          name={name}
          value="2"
          checked={value === '2'}
          onChange={handleChange}
        />
        女性
      </label>
      <label>
        <input type="radio"
          name={name}
          value="3"
          checked={value === '3'}
          onChange={handleChange}
        />
        ゴリラ
      </label>
    </div>
  );
};

// メール欄
const InputMail: React.FC<FormPartsProps<string>> = ({
  name,
  value,
  onChange,
  errMsg,
  onError,
}) => {
  const handleChange = useCallback(
    (e: React.ChangeEvent<HTMLInputElement>) => {
      if (validate(e.target.value)){
        onChange({ [name]: e.target.value });
      } else {
        onError({ [name]: 'AnyErrorMessage' });
      }
    },
    [name, onChange],
  );

  return (
    <div>
      <label>メール:</label>
      <input type="email" name={name} value={value} onChange={handleChange} />
      {errMsg ? <div>{errMsg}</div> : null}
    </div>
  );
};

// テキストエリア
interface TextAreaProps extends FormPartsProps<string> {
  placeholder?: string;
  maxlength?: number;
}

const TextArea: React.FC<TextAreaProps> = ({
  name,
  value,
  onChange,
  maxlength = 50,
  label = '備考',
}) => {
  const handleChange = useCallback(
    (e: React.ChangeEvent<HTMLTextAreaElement>) => {
      onChange({ [name]: e.target.value })
    },
    [name, onChange],
  );

  return (
    <div>
      <label>{label}:</label>
      <textarea
        name={name}
        value={value}
        onChange={handleChange}
        maxlength={maxlength}
      ></textarea>
    </div>
  );
};

フォーム部品の設置

フォーム部品の設置例。
入力値を管理するstateとエラーを管理するstateをそれぞれ用意してます。
未入力チェックなどはこちらで確認します。

const Form: React.FC<{ onSubmit: Function }> = ({ onSubmit }) => {
  const [data, setData] = useFormState<DataType>(initialData);
  const [error, setError] = useFormState<ErrorType>(initialError);
  const isButtonDisabled = Object.values(error).some(v => v);

  const handleSubmit = useCallback(
    (e: React.MouseEvent<HTMLFormElement>) => {
      e.preventDefault();
      if (validate(data)){
        onSubmit(data);
      } else {
        setError({[TargetName]: 'AnyErrorMessage'})
      }
    },
    [onSubmit, data],
  );

  return (
    <form onSubmit={handleSubmit}>
      <TextBox
        required
        name="last-name"
        label="姓"
        value={data.lastName}
        onChange={setState}
        errMsg={error.lastName}
        onError={setError}
      />
      <TextBox
        required
        name="first-name"
        label="名"
        value={data.firstName}
        onChange={setState}
        errMsg={error.firstName}
        onError={setError}
      />
      <SelectSex
        name="sex"
        value={data.sex}
        onChange={setState}
      />
      <InputMail 
        name="mail"
        value={data.mail}
        onChange={setState}
        errMsg={error.mail}
        onError={setError}
      />
      <TextArea
        name="description"
        label="その他"
        value={data.description}
        onChange={setState}
      />
      <button type="submit" disabled={isButtonDisabled}>
        送信
      </button>
    </form>
  );
};

Reactのフォームの実装は、こちらの記事にあるような元々のFormの機能を活用するのが一番シンプルで良いよなーとは思います。
ReactでできるだけシンプルにForm実装
今回は、WebAPIからサジェストリストを取得するInputコンポーネントなど、実装がややこしい部品のややこしい部分を隠蔽しつつ汎用化させて使いまわしたいなと思い、作ってみた次第です。

7
6
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
7
6