0
0

HTML <form>タグの基礎の勉強 (Zod Next.js conform)

Last updated at Posted at 2024-06-07

Next.jsの ServerActionはconformを使うと良いそうなので勉強します。
conform を使えるようになるために まずは、HTML <form>タグを一から勉強します。

HTML formタグの基礎

<form>
</form>

HTMLの基本タグである<form>は👆️の様に囲って使います。
この中に複数の項目をまとめて、ブラウザからサーバーに送信します。

ここには、👇️以下の属性を設定できます。

id:フォームに一意のIDを指定します。
action: 送信先となるサーバー側のプログラムのURLを指定します。省略すると、現在のページに送信されます。

method: 送信方法を指定します。GET または POST のいずれかを指定できます。
※Next.js Reactでは サーバー関数を実行できるように拡張されています。

name: フォームの名前を指定します。

target:送信先のフレームまたはウィンドウを指定します。

enctype: ファイルアップロードを行う場合に、エンコーディング方式を指定します。

accept-charset:送信データで許容される文字エンコーディングを指定します。

入力項目を作成する

主なHTMLタグ

<input>: テキスト入力、パスワード入力、チェックボックス、ラジオボタンなど、様々な入力項目を作成できます。
<select>: ドロップダウンリストを作成できます。
<textarea>: 複数行のテキスト入力エリアを作成できます。

Confromでは全てのHTMLタグを使えます。

HTMLタグには属性を追加します。

主な属性

id: 入力項目にIDを指定します。
type: 入力項目の種類を指定します。
name: 入力項目の名前を指定します。送信時にこの名前で値が渡されます。
value: 入力項目の初期値を指定します。

送信ボタン

これら複数の項目をまとめてサーバーに送ります。

HTMLのfor属性は、<label>要素がどのフォーム要素に関連付けられているかを指定します。
for属性の値は、関連付けるフォーム要素のid属性の値と一致する必要があります。

<label for="name">名前:</label>
<input type="text" id="name" name="name" />

※htmlFor属性は、HTMLのfor属性に対応するもので、ReactではhtmlForという名前で使用します。これはJavaScriptの予約語forとの衝突を避けるためです。

<button type="submit">送信</button>

formの項目を動的に追加、削除、変更する方法はJavaScriptを使用します。

クエリパラメータ

題名

form内に配置された各入力要素は、name属性とvalue属性を持ち、これらの値がクエリパラメータとして生成されます。

入力フォームとその生成されるクエリパラメータ

属性 説明
name 入力要素の名前 title
value 入力要素の値 test-title

ユーザーが入力値をtest-titleという名前で送信すると、次のようなURLが生成されます。

http://example.jp/article?title=test-title

例えば、検索サイトをブラウザで開き、検索キーワードを入力して
そのキーワードで検索すると、URLにクエリパラメータが追加されます。
そのクエリパラメータをサーバー側で解析し、
キーワードで検索を行います。
そして、検索結果をブラウザ上で表示します。

サーバー側での処理

サーバー側では、GETリクエストを受信すると、URLに含まれるクエリパラメータを解析し、対応する処理を実行します。


基本的なHTML <form>タグの使い方

基本的なHTML

タグの使い方を知らなければ
conformで何が便利になったか、どこが拡張されたのかわかりません。

基本的なHTMLタグを使った使用例

例1

export default function BasicForm() {
  return (
    <form action="/" method="post">
      <label htmlFor="name">名前:</label>
      <input type="text" id="name" name="name" />
      <br />
      <label htmlFor="email">メールアドレス:</label>
      <input type="email" id="email" name="email" />
      <br />
      <label htmlFor="message">メッセージ:</label>
      <textarea id="message" name="message" />
      <br />
      <button type="submit">送信</button>
    </form>
  );
}

例2

<form method="GET" action="http://example.jp/article">
    <label>題名 <input type="text" id="title" name="title"></label>
    <input type="submit" name="submit" value="検索">
</form>




参考

Next.jsで素朴なフォームをシンプルに作る
https://zenn.dev/moozaru/articles/58bd654bbc402c

参考サイトのコア部分を自分なりに勉強します。


Next.js Zodのインストール

pnpm dlx create-next-app nextjs14

pnpm install zod

基本

src/app/simple/page.tsx
export default function Page() {
  return (
    <form>
      <input type="text" name="name" />
      <button type="submit">送信</button>
    </form>
  );
}

Server Actionsの関数定義

src/app/simple/postAction.ts
'use server';

import { redirect } from 'next/navigation';

export async function postAction(formData: FormData) {
  const name = formData.get('name');
  console.log(name);

  // 入力されたデータが取得できたので
  // 何らかのデータ処理をします。

  // その後、リダイレクトします。
  redirect('/simple/thanks');
}

Server Actionsの使用条件

"use server"; でバックエンド側であることを宣言する。
非同期処理である async にする。
引数に FormData オブジェクトを渡す

Server Actionsをフォームタグ

から呼び出す
src/app/simple/page.tsx
import { postAction } from "@/app/simple/postAction";

export default function Page() {
  return (
    <form action={postAction}>
      <input type="text" name="name" />
      <button type="submit">送信</button>
    </form>
  );
}

これで基本的なServerActionの動作確認が出来ました。


thanksページの追加

入力成功後に表示するページ

src/app/simple/thanks/page.tsx
import Link from 'next/link';

export default function Page() {
  return (
    <div>
      投稿ありがとうございます
      <br />
      <Link href="/simple">フォームに戻る</Link>
    </div>
  );
}


Zodってなに?

Zodは、JavaScript及びTypeScript向けに開発された強力なスキーマベースのバリデーションライブラリです。データの構造を定義し、そのデータが期待通りの形式かどうかを検証する機能を提供します。

従来のバリデーションライブラリと異なり、Zodは以下のような利点を持ちます。

  • 直感的なスキーマ定義: スキーマはシンプルなオブジェクト形式で記述でき、コードの可読性とメンテナンス性を向上させます。
  • 強力な型: TypeScriptとの緊密な統合により、型安全性を高め、実行時エラーを抑制します。
  • 柔軟なバリデーションルール: 型チェックだけでなく、長さ、範囲、フォーマットなどの様々なバリデーションルールを定義できます。
  • 再利用可能なスキーマ: 定義したスキーマを様々な箇所で再利用することで、コードの重複を削減できます。
  • 詳細なエラーメッセージ: バリデーションエラー発生時に、詳細なエラーメッセージを出力し、問題箇所を特定しやすくなります。

Zodは、クライアントサイドとサーバーサイドの両方で使用でき、フォームデータのバリデーション、APIリクエストの検証、データ変換など、様々なユースケースで活用できます。

バリデーション

フォームに入力されたデータが正しいかどうかをチェックすることです。

Zodを使ったバリデーション

通常バリデーションは
クライアント
サーバー
両方で行うのが安心です。

クライアントで大まかにチェックして
サーバー側で詳細なチェックを行うのが理想です。

バリデーションにzodを使用します。

スキーマ

要するにバリデーションのルールを決めるってことです。
これをスキーマと呼びます。

👇️スキーマの例

const UserSchema = z.object({
  username: z.string().min(1, 'ユーザー名必須').max(20, '20文字以内')
});

このようにスキーマはそれぞれの項目の型定義+ルールみたいなもので、
後から追加や削除が簡単に出来ます。

スキーマを使ったバリデーション

受け取ったformDataにバリデーション関数?を使っています。

※バリデーション関数という使い方は初耳
スキーマ関数と呼んだほうが直感的か?

バリデーションを関数形式にして
引数にformDataを渡すという感じで使います。

基本的な形

UserSchema(formData)

👆️Zodを使ってバリデーション関数を作り
フォームに入力されたデータ(=formData)を引数として渡して
その結果でバリデーションが成功したかを判定します。


バリデーションの追加

先程のコードにバリデーションを追加します。

app\simple\postAction.ts
'use server';

import console from 'console';

import { redirect } from 'next/navigation';
import { z } from 'zod';

const UserSchema = z.object({
  username: z.string().min(1, 'ユーザー名必須').max(20, '20文字以内')
});

export async function postAction(formData: FormData) {
  const result = UserSchema.safeParse({
    username: formData.get('name') as string
  });
  // バリデーション判定
  if (!result.success) {
    console.log('バリデーションエラーがあります:', result.error);
  } else {
    console.log('バリデーション成功');
  }

  // ユーザーネームを取り出す1。バリデーション前のコード
  // 受け取ったformDataからnameを取り出します。
  console.log('==formData get name', formData.get('name'));

  const name = formData.get('name');

  // ユーザーネームを取り出す2。バリデーション後のコード
  // safeParse()でバリデーションを行い、成功した場合はresult.dataにデータが入ります。
  console.log('==result.data.username', result.data?.username);

  // 入力されたデータが取得できたので
  // 何らかのデータ処理をします。

  // その後、リダイレクトします。
  redirect('/simple/thanks');
}

これで基本的なバリデーションが出来ました。


スキーマから型を生成

スキーマから型を取得する

type InputsType = z.infer<typeof UserSchema>;
const inputs: InputsType = result.data as InputsType;
console.log('==inputs.username', inputs.username);

inferはスキーマから型を取得します。

👆️のコードは、UserSchemaから型を取得し、inputsに代入しています


useFormState

useFormStateは、Server Actionsの返り値をリアクティブに扱えるようにしてくれます

リアクティブ

データの変化に自動的に反応し、それに応じて処理を実行します。

つまり、useFormState を使うと、Server Actions の結果が更新されるたびに、それに応じた処理を自動的に実行することができます。


色々追加して適当に完成した

を利用したソース
app\simple\page.tsx
'use client';

import { useFormState, useFormStatus } from 'react-dom';

import { postAction } from './postAction';

function Submit() {
  const status = useFormStatus();
  return (
    <button type="submit" disabled={status.pending}>
      {status.pending ? '送信中...' : '送信'}
    </button>
  );
}

export default function Page() {
  const [result, dispatch] = useFormState(
    (state: any, payload: FormData) => postAction(state, payload),
    undefined
  );

  return (
    <form action={dispatch}>
      {result && result.errors && <div>{result.errors.name}</div>}
      <input type="text" name="name" />
      <Submit />
    </form>
  );
}

app\simple\postAction.ts
'use server';

import console from 'console';

import { redirect } from 'next/navigation';
import { z } from 'zod';

interface ReturnType {
  errors?: {
    name?: string;
  };
}

const UserSchema = z.object({
  username: z.string().min(1, 'ユーザー名必須').max(20, '20文字以内')
});

export async function postAction(
  prevData: ReturnType,
  formData: FormData
): Promise<ReturnType> {
  // バリデーション
  const result = UserSchema.safeParse({
    username: formData.get('name') as string
  });
  // バリデーション判定
  if (!result.success) {
    console.log('バリデーションエラーがあります:', result.error);
  } else {
    console.log('バリデーション成功');
  }

  // ユーザーネームを取り出す1。バリデーション前のコード
  // 受け取ったformDataからnameを取り出します。
  console.log('==formData get name', formData.get('name'));

  const name = formData.get('name');

  // ユーザーネームが空でないことを確認
  if (!name) {
    // ユーザーネームが空の場合はもう一度入力を促す
    redirect('/simple/');
  }
  // ユーザーネームを取り出す2。バリデーション後のコード
  // safeParse()でバリデーションを行い、成功した場合はresult.dataにデータが入ります。
  console.log('==result.data.username', result.data?.username);

  // スキーマから型を取得する
  type InputsType = z.infer<typeof UserSchema>;
  const inputs: InputsType = result.data as InputsType;
  console.log('==inputs.username', inputs.username);

  // 入力されたデータが取得できたので
  // 何らかのデータ処理をします。

  // その後、リダイレクトします。
  redirect('/simple/thanks');
}

app\simple\thanks\page.tsx
'use client';

import Link from 'next/link';

export default function Page() {
  return (
    <div>
      投稿ありがとうございます
      <br />
      <Link href="/simple">フォームに戻る</Link>
    </div>
  );
}

これで基本的なHTML <from>タグの勉強は終了です。
この基礎的な<form>タグを元に次はconformを勉強していきます。

※メッセージ等、所々足りてません。

※useFormStateとuseFormStatusがなくなる?
useActionStateという新しい名前に変わるそうです。

この基礎的な <form>タグ の知識を身に着けた上で、comformを勉強していきます。

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