1. EndouT6

    Posted

    EndouT6
Changes in title
+React Hooksを利用したフォーム実装
Changes in tags
Changes in body
Source | HTML | Preview
@@ -0,0 +1,312 @@
+こんにちはdohdonです。
+
+今回はReactでのフォーム作成についての記事です。
+下記に当てはまる人が対象です。
+
+- Reactでフォームを作りたい
+- フォームの管理をストアに持たせたくない。
+- プラグインも使いたく無い
+
+hooksを利用して作るフォームが状態管理の切り分けができて良かったのと
+バリデーション実装ロジックでアハ体験があったので記事にしました。
+
+## フォームの基礎(バリデーション無し)
+
+さっそくフォームを作ってみましょう。
+バリデーションをかけない場合は入力情報の保存・更新ができればOKです。
+
+さっそくuseStateで実装してみます。
+今回はログインフォームを作ってみましょう
+よくあるメールとパスワードを入力してログインする形ですね。
+
+```component.js
+import React, { useState } from 'react'
+
+const Example = ({onSubmit}) => {
+ [state, setState] = useState({
+ mailaddress: '',
+ password: '',
+ })
+ 
+ const submit = () => onSubmit({
+ mailaddress: state.mailAddress,
+  password: state.password
+ })
+ 
+ const updateMailaddress = (ev) => setState({
+ ...state
+ mailaddress: ev.target.value
+ })
+
+ const updatePassword = (ev) => setState({
+ ...state
+ password: ev.target.value
+ })
+ 
+ return (
+ <div>
+ // メールアドレス
+ <input value={state.mailaddress} onChange={setMailaddress}/>
+ // パスワード
+ <input type="password" value={state.password} onChange={setPassword}/>
+ // 決定ボタン
+ <button onClick={submit}>ログインする</button>
+ </div>
+ )
+}
+
+```
+
+簡易のフォームであればこんな形になるかと思います。
+ただ、ログインフォームを空で渡すわけにもいきませんし、やはりバリデーションが必要です。
+
+サーバー側からエラーを返してもらって表示するという形も想定されますが
+無用な通信を発生させないためにも最低限フロント側でバリデーションはかけたいところです。
+
+## フォームを管理する上で用意する情報構造
+
+実際にバリデーション機能付きのフォームを作る上で管理が必要な項目を説明をしていきます。
+必要な情報は次のものです。
+
+- 入力情報
+- エラー情報
+- エラー表示判定情報
+
+#### 入力情報
+ユーザーが入力した情報が格納されます。
+コンポーネント側ではinput要素のvalueの値となります。
+
+#### エラー情報
+表示するエラーメッセージを入れます。
+今回は必須チェックのみとするので「必須です」が入ります。
+エラーがないときはnullを入れます。
+コンポーネント側では格納されたエラーがある時エラーを表示します。
+
+#### エラー表示判定
+その名の通りエラーを表示するかどうかを判定する部分です。
+ユーザーがフォーカスを外した時、決定ボタンが押された時、エラーを表示したいタイミングは一律ではありません。
+コンポーネント側ではここがtrueであればエラーを表示するようにします。
+
+以上の構造を鑑みて管理するデータ構造は次のよう型定義になります。
+
+```State.ts
+interface State {
+ // 入力情報
+ inputs: {
+ mailaddress: string
+ password: string
+ }
+
+ // エラー情報
+ errors: {
+ mailaddress: string | null
+ password: string | null
+ }
+
+ // エラー表示判定(今回はユーザーが触れたかどうかで表示判定したいためtouchedと名付けます)
+ touched: {
+ mailaddress: boolean
+ password: boolean
+ }
+}
+```
+
+入力・エラー・エラー表示判定はそれぞれ同じKeyを持っていることが確認できますね。
+
+
+## 状態管理のための外部ファイルを作成
+
+先ほどはuseStateを使っていましたが、構造が複雑になってきたのでuseReucerを使います。
+型定義があったほうが分かりやすいので、今回はtypescriptで記述していきます。
+
+```useLoginForm.ts
+
+import { useReducer } from 'react'
+
+interface Action<T, P, E = {}> {
+ type: T;
+ payload: P;
+ error?: E;
+}
+type PatchLogin = Action<'PATCH_LOGIN', Partial<{
+ mailaddress: string
+ password: string
+}>>
+type TouchAll = Action<'TOUCH_ALL', undefined>
+type Actions = PatchLogin | TouchAll
+
+interface State {
+ inputs: {
+ mailaddress: string
+ password: string
+ }
+ errors: {
+ mailaddress: string | null
+ password: string | null
+ }
+ touched: {
+ mailaddress: boolean
+ password: boolean
+ }
+}
+
+// バリデーション: 必須チェック
+const required = (v) => {
+ return v === null || v === undefined || v === '' || typeof v === 'number' && isNaN(v) ? '必須です' : null
+}
+
+const reducer = (state: State, action: Actions): State => {
+ switch (action.type) {
+ case 'PATCH_LOGIN':
+ inputs {
+ inputs: { ...state.inputs, ...action.payload },
+ errors: {
+ ...state.errors,
+ mailaddress:
+ action.payload.mailaddress || action.payload.mailaddress === ''
+ ? required(action.payload.mailaddress)
+ : required(state.inputs.mailaddress),
+ password:
+ action.payload.password || action.payload.password === ''
+ ? required(action.payload.password)
+ : required(state.inputs.password)
+ },
+ touched: {
+ ...state.touched,
+ [Object.keys(action.payload)[0]]: true
+ }
+ }
+ case 'TOUCH_ALL':
+ return {
+ ...state,
+ touched: {
+ ...state.touched,
+ ...Object.keys(state.touched).reduce(
+ (o, k) => ({ ...o, [k]: true }),
+ {}
+ )
+ }
+ }
+ }
+}
+
+export const useLoginForm = (initialState?: Login) =>
+ useReducer(reducer, {
+ inputs: initialState || {
+ mailaddress: '',
+ password: ''
+ },
+ errors: {
+ mailaddress: null,
+ password: null
+ },
+ touched: {
+ mailaddress: false,
+ password: false
+ }
+ })
+
+```
+
+少し複雑なように見えますがやっていることは先ほど説明した内容と一緒です。
+
+hooksファイルに入力情報管理用のロジックを切り分けることで
+コンポーネントが持つロジックの切り分けが楽にできます。
+
+次にコンポーネント側を見ていきます。
+
+```component.tsx
+
+import React, { FC, useCallback, useEffect } from 'react'
+
+interface Props {
+ onSave(): any
+}
+
+export const OrderInputForm: FC<Props> = ({
+ onSave,
+}) => {
+ useEffect(() => {
+ dispatch({
+ type: 'PATCH_LOGIN',
+ payload: {}
+ })
+
+ onLoad()
+ }, [])
+
+ const [{ inputs, errors, touched }, dispatch] = useLoginForm({
+ mailaddress: '',
+ password: ''
+ })
+
+ const updateMailaddress = useCallback(ev => {
+ dispatch({
+ type: 'PATCH_LOGIN',
+ payload: { mailaddress: ev.target.value }
+ })
+ }, [])
+
+ const updatePassword = useCallback(ev => {
+ dispatch({
+ type: 'PATCH_LOGIN',
+ payload: { password: ev.target.value }
+ })
+ }, [])
+
+
+ const submit = () => {
+ dispatch({
+ type: 'TOUCH_ALL',
+ payload: undefined
+ })
+
+ //
+ if (Object.values(errors).every(e => e === null)) {
+ onSave()
+ }
+ }
+
+ return (
+ <div>
+ // メールアドレス
+ <input value={inputs.mailaddress} onChange={updateMailaddress}/>
+     {errors.mailaddress ? <p>{errors.mailaddress}</p> : false}
+ // パスワード
+ <input type="password" value={inputs.password} onChange={updatePassword}/>
+     {errors.password ? <p>{errors.password}</p> : false}
+ // 決定ボタン
+ <button onClick={submit}>ログインする</button>
+ </div>
+ )
+}
+
+```
+
+処理の流れを確認していきましょう。
+
+1. ユーザーが画面を表示する
+2. 全ての項目にバリデーションを走らせる。今回は各項目に「必須です」という文字列が入る。
+3. ユーザーがフォームに入力する
+4. 入力があった際は再度バリデーションが走る。問題がない項目はエラー情報にnullが入り、タッチ情報がtrueになる。
+5. 決定ボタンが押されたとき、エラー表示判定を全てtrueにし、エラーが表示状態になる。errorが全てnullであれば入力情報を送信する。
+
+という形です。
+これで無用な通信を制限することのできるフォームができたかと思います。
+
+## 最後に
+今回個人的にアハ体験だったのが、エラー表示判定に使っているtouchedの部分です。
+
+実は自分で実装するとき、Submitボタンを押された時の処理が上手くいかず
+やり方を模索していたのですが、考えつくロジックがどれも微妙で、「実装はできるけど、この作り方は良くない病」にかかってました。
+
+悩んでチームリーダーに相談したところ、formikだとこの仕組みで作っているよ、と聞き1発で解決したわけです。
+
+前々から言われてはいたのですが
+色々使っているものの中身を見た方が知見が広まるんだなーと実感した瞬間でした。
+コード見る力も養えますもんね。
+見ていきましょう。
+
+間違ってる箇所などありましたら指摘いただけると幸いです!
+
+ここまで読んでくださりありがとうございました。