ども!年末の追い込みが激しくどかどか働いている龍ちゃんです。寒すぎて暖房で過ごしていますが、電気代におびえています。 皆さん、 React 19のドキュメント は確認しましたか?12/5についに React 19がstableになりました 。僕はお恥ずかしながら、Next.jsの検証環境を作っていた時にNext15がStableになっていることで気づきました。Stableになったので、そろそろ学ぼうということで新しいHookを学んでいこうと思います。 今回の内容は、「新しいHookである、useActionStateを使ってTypescriptで入力フォームを作る」という内容になります。極力Typescriptで型を付けた状態で紹介していきます。 useActionStateについて確認 まずは、基本の使い方についてまとめていきます。いろいろと利用シーンがあると思います。ざっくりとuseActionStateのうれしいところは以下の点です。 useActionState内で非同期処理の状態を取得することができる ライブラリを使用せずにReactのみでフォームの管理が楽になった ここでは二つの例を紹介していきます。二つの例の違いとしては、Formの入力値の使用の有無になります。 どちらの使い方でも共通しているのは、useStateと同じようにStateを保存することができます。イメージとしては、asyncの処理を連携したState更新処理をForm(入力)とセットで管理することができるHookとなります。 Formの値を使用しないuseActionState 例として、カウントアップを作成します。こちらの内容は、Reactの公式ドキュメントにも記載されています。 useActionStateで 設定がマストな引数 は2つになります。第一引数には action 関数、第二引数には初期値を渡します。 action 関数では、初回の実行時には初期値が渡り、それ以降は前回のaction関数で返答された値が返答されます。型付け行った場合は、 action 関数の戻り値は型と一致する必要があります。 import { useActionState } from "react"; export const FormTest1 = () => { const [count, countAction, isCountPending] = useActionState<number>( async (prevCount: number) => { await new Promise((res) => setTimeout(res, 1000)); console.log(prevCount); return prevCount + 1; }, 0 ); return ( <> <form action={countAction}> <button type="submit" disabled={isCountPending}> カウントアップ </button> <p>{count}</p> </form> </> ); }; 今回のaction関数はuseActionStateの機能を試すために、処理を2秒止めています。useActionStateからの返り値は3つです。 count:Stateの値 初回では初期値が、実行後はaction関数によって更新された値が挿入 countAction:action関数の実行トリガー isCountPending:action実行状態を取得 useActionStateの素晴らしい点としては、ソース中の isCountPending にあります。これまで、useStateやuseRefを組み合わせて作成していた。loading表示などもこちらを使用することで一つにまとめることができます。 Formの値を使用するuseActionState 例としては、簡易的なバリデーションがついたフォームとなります。実用性はありませんが、useActionStateの挙動を理解する手助けと、Typescriptでの型検証には有用だと思います。 フォームの入力情報を受け取る場合は、action関数の第二引数に情報が飛んできます。Stateとしては、 Error | null の状態を持つことで、バリデーションの有無を表現しています。 import { useActionState } from "react"; export const FormTest1 = () => { const [error, action, isPending] = useActionState<Error | null, FormData>( async (prevError: Error | null, formData: FormData) => { console.log(prevError); // 値の取得方法法 const data = Object.fromEntries(formData.entries()); console.log(data); // APIの処理などを行ってResultによって処理を分岐させる // returnを返せばエラー発生とする const error = new Error("Failed to submit data"); if (error) { return error; } return null; }, null ); return ( <> <form action={action}> <input type="text" name="name" /> <button type="submit" disabled={isPending}> 送信 </button> {error && <p>{error.message}</p>} </form> </> ); }; この例で確認できる内容としては以下になります。 useActionStateで送信されるForm情報の構造化 State設定の自由度がそれなりにある useActionStateで入力フォームを作成する 2つの例でuseActionStateの挙動については理解できたと仮定して、実際利用するフォームの作成を進めていきます。 作成するフォームの情報をまとめます。 名前:String・年齢:number バリデーション 名前:空文字禁止・10文字以内 年齢:0以上 バリデーション通過後、API通信をするイメージ(今回は2秒後エラー) ソースコードの全体を先に置いておきます。 import { useActionState } from "react"; type FormType = { name: string; age: number; }; type PrevFormDataType = { value: FormType; validationError: { name: Error | null; age: Error | null }; apiError: Error | null; }; const validationName = (name: string) => { if (name === "") { return new Error("名前を入力してください"); } else if (name.length > 10) { return new Error("名前は10文字以内で入力してください"); } return null; }; const validationAge = (age: number) => { if (age <= 0) { return new Error("年齢は0以上で入力してください"); } return null; }; export const FormTest3 = () => { const initialFormData: PrevFormDataType = { value: { name: "", age: 0 }, validationError: { name: null, age: null }, apiError: null, }; const [formData, action, isPending] = useActionState< PrevFormDataType, FormData >(async (_: PrevFormDataType, formData: FormData) => { // FormDataをobjectに変換 const _formData = Object.fromEntries(formData.entries()); const data: FormType = { name: _formData.name as string, age: Number(_formData.age), }; // validationを掛ける いい感じのライブラリがあれば参考にする const nameError = validationName(data.name); const ageError = validationAge(data.age); if (nameError || ageError) { return { value: { name: data.name, age: data.age }, validationError: { name: nameError, age: ageError, }, apiError: null, }; } // ここでAPI処理を実装・今回は2秒待ってエラーを返す await new Promise((res) => setTimeout(res, 2000)); const apiError = new Error("Failed to submit data"); return { value: { name: data.name, age: data.age }, validationError: { name: nameError, age: ageError, }, apiError: apiError, }; }, initialFormData); return ( <> <form action={action} className="flex w-full max-w-xl flex-col gap-2 rounded-md p-4 shadow" > <label className="flex flex-col"> <div className="flex flex-row text-xl"> <span className="w-1/3">名前:</span> <input className="w-full border p-1 text-right" type="text" name="name" defaultValue={formData.value.name} /> </div> <span className="h-4 text-xs text-red-500"> {formData.validationError.name && ( <>{formData.validationError.name.message}</> )} </span> </label> <label className="flex flex-col"> <div className="flex flex-row text-xl"> <span className="w-1/3">年齢:</span> <input className="w-full border p-1 text-right" type="number" name="age" defaultValue={formData.value.age} /> </div> <span className="h-4 text-xs text-red-500"> {formData.validationError.age && ( <>{formData.validationError.age.message}</> )} </span> </label> <button className={ "w-full rounded-md py-4 text-lg text-white" + (isPending ? " bg-gray-400" : " bg-blue-500") } type="submit" formAction={action} disabled={isPending} > 送信{isPending && "中"} </button> <span className="h-4 text-xs text-red-500"> {formData.apiError && <p>{formData.apiError.message}</p>} </span> </form> </> ); }; Stateの型定義 useActionStateでaction関数実行後は、Formの入力値はリセットが掛かってしまいます。フォームのバリデーション評価の間は値を継続させたいので、Stateの定義としてはFormの入力値・バリデーションエラー・apiエラーの三つを取得できるオブジェクトとして定義しておきます。 // Formのタイプ type FormType = { name: string; age: number; }; // Stateの型定義 type PrevFormDataType = { value: FormType; validationError: { name: Error | null; age: Error | null }; apiError: Error | null; }; バリデーション 将来的にはライブラリを使って運用を進めていきたいのですが、ここでは簡易的に自作したバリデーションを使用します。 名前のバリデーション const validationName = (name: string) => { if (name === "") { return new Error("名前を入力してください"); } else if (name.length > 10) { return new Error("名前は10文字以内で入力してください"); } return null; }; 年齢のバリデーション const validationAge = (age: number) => { if (age <= 0) { return new Error("年齢は0以上で入力してください"); } return null; }; バリデーション関数としては、型定義と合わせて Error | null を戻り値として設定しています。 useActionStateの実装 初期化の値を別途定義しています。初期状態では、各種エラーは null を入れておきます。フォームの値も初期値を設定します。 const initialFormData: PrevFormDataType = { value: { name: "", age: 0 }, validationError: { name: null, age: null }, apiError: null, }; const [formData, action, isPending] = useActionState< PrevFormDataType, FormData >(async (_: PrevFormDataType, formData: FormData) => { // FormDataをobjectに変換 const _formData = Object.fromEntries(formData.entries()); const data: FormType = { name: _formData.name as string, age: Number(_formData.age), }; // validationを掛ける いい感じのライブラリがあれば参考にする const nameError = validationName(data.name); const ageError = validationAge(data.age); if (nameError || ageError) { return { value: { name: data.name, age: data.age }, validationError: { name: nameError, age: ageError, }, apiError: null, }; } // ここでAPI処理を実装・今回は2秒待ってエラーを返す await new Promise((res) => setTimeout(res, 2000)); const apiError = new Error("Failed to submit data"); return { value: { name: data.name, age: data.age }, validationError: { name: nameError, age: ageError, }, apiError: apiError, }; }, initialFormData); action 関数の中身としては、以下のような流れになっています。 formDataの積み替え → FormTypeの情報へ変換 バリデーションチェック・早期リターンでバリデーションエラー表示 API通信 今回は、検証の意味を込めてAPIエラーも用意しています。ここは使用用途によって、ErrorBoundaryでキャッチする仕様でも問題ないかと思います。 終わり 今回は、useActionStateをTypescriptで型付けしながら入力フォームの実装をしてみました。新しい機能が出ても、Stableまで手を出さないというのは、良いことなのか悪いことなのかわかりませんね。きっと、技術選定でリジェクトされた思い出が強く残っているのだと思います。 useActionState以外にも便利そうなHooksが追加されていたので、React 19とNext 15で色々作ってみるのも楽しそうですね。ふんわりと年末に入りますが、一旦はメリークリスマス! ご覧いただきありがとうございます! この投稿はお役に立ちましたか? 役に立った 役に立たなかった 0人がこの投稿は役に立ったと言っています。 The post React 19でuseActionStateで入力フォーム【Typescript】 first appeared on SIOS Tech. Lab .