フォーム部品のstateを一つのstateとハンドラで管理するにあたり、TypeScriptでなるべく型定義してみました。
なお、各HTML要素のインターフェース名はこちらで確認できます。
Web API | MDN
フォーム部品のインターフェース
各フォーム部品が共通して実装するインターフェースの定義。
name
とvalue
、onChange
が肝で、後はまあ共通して必要そうなものをいくつか。
それぞれのフォーム部品が扱うデータ型をジェネリック(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コンポーネントなど、実装がややこしい部品のややこしい部分を隠蔽しつつ汎用化させて使いまわしたいなと思い、作ってみた次第です。