Reactにとって、複数のフォーム制御はけっこう処理が面倒な部分でした。
ですが今までのフォーム制御の知識は全部捨てて下さい。全てuseActionStateフックで解決します。
React19でこのuseActionStateフックが登場し、以下の方法を知ったときは遂にReactもVue、Angular、Svelteと同じぐらいフォーム制御が簡潔になったかと大いに感動を覚えたものです(動きとしてはAngularのreactiveFormに近い)。
ところが、色々と技術系サイトを巡回した所、この手法がなぜかほとんど知られていないような気がしたので記事にしてみました。
従来の記法
ちなみに、今までReactでのフォーム制御といえば
const [hogera,setHoge] = useState('');
<input onChange={(e)=>setHoge(e.target.value)} value={hoge} />
こんな書き方をしたと思います。onChangeイベントハンドラで値をステート変数化し、セッタ関数で同期をとって、それをvalueに返す……そしてフォームが複数になれば、こういった制御をずらずらと書いたりもしていました(useStateフックを一元化する方法もあります)。
ですがuseActionStateフックを使えばもうそんな冗長な記述は要りません。
- onChangeイベントハンドラ
- valueプロパティ
- useState
- セッタ関数
これらは全部不要となります。
そしてuseActionStateフックに用意されているisPendingという制御フラグですが、単に状態遷移目的のメッセージ表示切換に用いるだけではなくフォーム入力準備完了の判定にも用いることができます。
実装フォームの例
具体的な実装フォームの例を挙げてみます。至ってオーソドックスなフォーム制御の記述です。
import { useState,useEffect,useActionState } from 'react';
//具体的なアクション
async function feedback(dif,formData){
dif.item1 = formData.get('item1')
dif.item2 = formData.get('item2')
dif.item3 = formData.get('item3')
return dif;
}
export default function EditTodo(){
const [initial,setInitial] = useState([])
const [dif,formAction,isPending] = useActionState(feedback,initial)
//更新を検知
useEffect(()=>{
if(isPending){
//フォーム転送後の処理
}
},[isPending])
return (
<>
<h2>useActionStateを用いたフォーム制御</h2>
{
<form action={formAction} >
<input id="item1" name="item1" defaultValue={initial.item1} />
<input id="item2" name="item2" defaultValue={initial.item2} />
<input id="item3" name="item3" defaultValue={initial.item3} />
<button type="submit">更新する</button>
</form>
}
</>
)
}
解説
useActionStateの働きを分解するとこうなります。
const [dif,formAction,isPending] = useActionState(feedback,initial)
const [更新差分,フォーム,処理判定フラグ] = useActionState(フォーム処理,初期値)
フォーム
フォーム上にはonChangeイベントハンドラもvalueも指定していないので、それ自体は何の同期制御もしていない通常フォーム扱いとなります。したがって、Reactの制御から外れているので、自由に文字を書き換えできます。
また、defaultValueも初期値を出力するだけのプロパティ(新規登録や検索の場合は不要)なので、機能としては独立しています。
フォーム処理のアクション
フォームがイベントによってsubmitされることでformタグ上のactionプロパティに設定されたformActionがコールされ、フォーム設定用のアクションが実行されます。具体的には以下の部分です。
//具体的な制御アクション
async function feedback(dif,formData){
dif.item1 = formData.get('item1') //nameプロパティで設定したフォームの値
dif.item2 = formData.get('item2') //nameプロパティで設定したフォームの値
dif.item3 = formData.get('item3') //nameプロパティで設定したフォームの値
return dif; //差分を代入し、返す
}
もし、このフォームがいくつも存在した場合、ずらずらと記述していくのは面倒になります。その場合は以下のようにループ文でも制御可能です。
async function feedback(dif,formData){
for( const[key,value] of formData.entries() ){
dif[key] = value; //フォームの値を順次代入していく
}
return dif;
}
※【注】差分更新用のオブジェクトは分割してはいけません。判定フラグが動かなくなります。
async function feedback(dif,formData){
const tmp = {...dif};
for( const[key,value] of formData.entries() ){
tmp[key] = value; //フォームの値を順次代入
}
return tmp; //分割された値は判定しない(isPendingがtrueにならない)
}
また、returnを忘れると一回だけの処理で止まってしまい、連続で動作できなくなります。
更新の検知
フォーム処理のアクションによって値が返されると、isPendingはtrueを返します。したがって、useEffectフックが機能するようになり、フォーム転送後の処理を実行できます。ここに修正や検索などの結果を返すようにします。
//更新を検知
useEffect(()=>{
if(isPending){
//フォーム転送後の具体的処理
}
},[isPending])
また、このようにフォーム処理をわけておくと、別コンポーネントからのデータ取得、トップコンポーネントへのリデューサ送出などにおいて非常に至便となります。