[Remix] conformを使ったログインフォームの実装
はじめに
この記事は、Remixアプリケーションのフォーム送信や、バリデーション処理などの一連のフォーム処理の実装について、conformやzodを使った実装についてまとめた記事となります。
今回のソースコード
今回紹介するソースコードは、以下のGihtubのリポジトリで公開しています。
また、記事での説明のためにコードの内容を一部修正しているため、リポジトリのコードと異なる点があることはご了承ください。
今回の実装環境
今回の実装にあたり利用したライブラリのバージョン等の環境は通りです。
- Node.js:v22.13.0
- @remix-run/node: 2.15.2
- @remix-run/react: 2.15.2
- @remix-run/serve: 2.15.2
- remix-auth: 4.1.0
- remix-auth-form: 3.0.0
- @conform-to/react: 1.2.2
- @conform-to/zod: 1.2.2
Conformについて
conformは、フォーム実装に関するnpmライブラリです。詳細は、公式ドキュメントを見ていただくのが一番だと思います。
その中で、「conform」について自分が良いと思った点は、「フォームの送信データの検証が、フロントエンドとサーバーサイドのどちらも同一ロジックで実装可能である点」 です。
機能実装をしている際に、フォームの送信データの検証を、フロントエンドとサーバーサイドのどちらも行うのが一般的ですが、その検証ロジックを両者で別々に実装する必要があって、「処理の内容は同じなのになぁ…」みたいなことが往々にしてありました。
そのため、Conformを用いることで、フォーム送信データの検証を1つにまとめることができる点は、とても魅力的に感じます。
Conformを使ったフォーム実装
今回実装するログインフォーム
今回実装するログインフォームの概要は、以下の通りです。基本的な入力項目として「メールアドレス」と「パスワード」で、ログインユーザーの認証をする素朴な形式です。
今回のログインフォームページの実装
まず、今回実装した、ログインフォームページの実装の全体像は、以下の通りとなります。
import { Form, useActionData } from '@remix-run/react';
import { ActionFunctionArgs, redirect } from '@remix-run/node';
import { useForm } from '@conform-to/react';
import { parseWithZod } from '@conform-to/zod';
import { z } from 'zod';
import { authenticator } from '~/services/auth.server';
import { sessionStorage } from '~/services/session.server';
// zodフォームスキーマ定義
const schema = z.object({
email: z
.string({ message: 'メールアドレスを入力してください' })
.email({ message: 'メールアドレスの形式で入力してください' }),
password: z.string({ message: 'パスワードを入力してください' }),
});
// Remixの「ログインページ」のコンポーネント
export default function LoginPage() {
// 前回の送信データを取得する
const lastResult = useActionData<typeof action>();
// `conform`のFormロジックを初期設定する
// この際に、上記の「zodフォームスキーマ定義」を追加することで、
// フォームの入力項目・バリデーションを設定することができる
const [form, fields] = useForm({
lastResult,
onValidate({ formData }) {
return parseWithZod(formData, { schema });
},
shouldValidate: 'onBlur',
shouldRevalidate: 'onInput',
});
// ページのHTMLを定義
// conformで定義したフォーム入力項目と、HTML(入力UI, エラーメッセージ表示など)と連携する
return (
<div >
<Form action="/login" method="post" id={form.id} onSubmit={form.onSubmit}>
<div>
<div>
<div>
{form.errors && (
<div>
{form.errors.map((error) => (
<FormMessage key={error}>{error}</FormMessage>
))}
</div>
)}
</div>
<div>
<div>
<label htmlFor="email">メールアドレス</label>
<input
id="email"
key={fields.email.key}
name={fields.email.name}
type="email"
placeholder="メールアドレスを入力してください"
aria-invalid={!!fields.email.errors}
/>
{fields.email.errors && (
<FormMessage>{fields.email.errors}</FormMessage>
)}
</div>
<div>
<label htmlFor="password">パスワード</label>
<input
id="password"
key={fields.password.key}
name={fields.password.name}
type="password"
placeholder="パスワードを入力してください"
aria-invalid={!!fields.password.errors}
/>
{fields.password.errors && (
<p>{fields.password.errors}</p>
)}
</div>
</div>
</div>
<div>
<button type="submit">ログイン</button>
</div>
</div>
</Form>
</div>
);
}
// action定義
// フォーム送信が実行されたら、actionの内容が実行される
export async function action({ request }: ActionFunctionArgs) {
// フォームの送信データを複製(`clone`)して取得
// 複製をしないと、後述のログイン認証処理内で、フォーム送信データを参照できなくなってしまう
const formData = await request.clone().formData();
// zodフォームスキーマ定義を使って、フォーム送信データを検証
const submission = parseWithZod(formData, { schema });
// バリデーションエラーの場合は、以降の処理を行わずに、
// サーバー側の検証結果を、フロント側に返答する
if (submission.status !== 'success') {
return submission.reply();
}
try {
// ログイン認証処理を実行
const user = await authenticator.authenticate('user-pass', request);
// ログイン認証処理に成功したら、クッキーにセッション情報を追加し、
// ログイン後ページへリダイレクト
const session = await sessionStorage.getSession(
request.headers.get('cookie')
);
session.set('user', user);
throw redirect('/', {
headers: { 'Set-Cookie': await sessionStorage.commitSession(session) },
});
} catch (error) {
if (error instanceof Error) {
// ログイン認証処理に失敗した場合は、エラーを追記して返す
return submission.reply({
formErrors: ['メールアドレス、またはパスワードが違います'],
});
}
// 上記以外の意図しないエラーの場合は、再送出する
throw error;
}
}
次に、個別の詳細説明を行います。
zodによるフォームのスキーマ定義
まず初めに、zodによるフォームのスキーマ定義を行います。
ここで、フォームの入力項目・バリデーションを設定することができます。
// zodフォームスキーマ定義
const schema = z.object({
email: z
.string({ message: 'メールアドレスを入力してください' })
.email({ message: 'メールアドレスの形式で入力してください' }),
password: z.string({ message: 'パスワードを入力してください' }),
});
conformを使ったフォーム定義の実装
次に「conformを使ったフォーム定義の実装」について説明します。
Remixのログインページのコンポーネントで以下の様に実装します。
export default function LoginPage() {
// 前回の送信データを取得する
const lastResult = useActionData<typeof action>();
// `conform`のFormロジックを初期設定する
// この際に、上記の「zodフォームスキーマ定義」を追加することで、
// フォームの入力項目・バリデーションを設定することができる
const [form, fields] = useForm({
lastResult,
onValidate({ formData }) {
return parseWithZod(formData, { schema });
},
shouldValidate: 'onBlur',
shouldRevalidate: 'onInput',
});
//~~~~
まず、useActionData
は、Remix関数で、前回のフォーム送信データを取得する処理となります。フォーム送信時、サーバー側の検証処理でエラーが起きた場合、処理結果をここで受け取ることで、ページにエラー内容を表示することができます。
次に、conform
によるFormロジックを初期設定を行います。useActionData
での前回のフォーム送信結果や、zodのフォームのスキーマ定義を指定、その他のフォームのオプションを指定します。
ログインフォームのHTML定義
次に、ログインフォームのHTML定義を行います。
// ページのHTMLを定義
// conformで定義したフォーム入力項目と、HTML(入力UI, エラーメッセージ表示など)と連携する
return (
<div >
<Form action="/login" method="post" id={form.id} onSubmit={form.onSubmit}>
<div>
<div>
<div>
{form.errors && (
<div>
{form.errors.map((error) => (
<FormMessage key={error}>{error}</FormMessage>
))}
</div>
)}
</div>
<div>
<div>
<label htmlFor="email">メールアドレス</label>
<input
id="email"
key={fields.email.key}
name={fields.email.name}
type="email"
placeholder="メールアドレスを入力してください"
aria-invalid={!!fields.email.errors}
/>
{fields.email.errors && (
<FormMessage>{fields.email.errors}</FormMessage>
)}
</div>
<div>
<label htmlFor="password">パスワード</label>
<input
id="password"
key={fields.password.key}
name={fields.password.name}
type="password"
placeholder="パスワードを入力してください"
aria-invalid={!!fields.password.errors}
/>
{fields.password.errors && (
<p>{fields.password.errors}</p>
)}
</div>
</div>
</div>
<div>
<button type="submit">ログイン</button>
</div>
</div>
</Form>
</div>
);
}
まず、上記のuseForm
で定義したfields
変数から、fields.<入力項目名>
で入力項目情報にアクセスできるので、入力項目情報と入力UIやエラーメッセージの表示と連携させます。
そして、<Form>
タグと、useForm
のform
変数を連携して、submitボタン時のフォーム送信処理を連携させます。
また、form.errors
については、サーバー側で個別に定義したエラー情報です。ログイン失敗時のエラー表示に利用します。
サーバー側のバリデーション
最後に、サーバー側の処理として、action
の定義を行います。
// action定義
// フォーム送信が実行されたら、actionの内容が実行される
export async function action({ request }: ActionFunctionArgs) {
// フォームの送信データを複製(`clone`)して取得
// 複製をしないと、後述のログイン認証処理内で、フォーム送信データを参照できなくなってしまう
const formData = await request.clone().formData();
// zodフォームスキーマ定義を使って、フォーム送信データを検証
const submission = parseWithZod(formData, { schema });
// バリデーションエラーの場合は、以降の処理を行わずに、
// サーバー側の検証結果を、フロント側に返答する
if (submission.status !== 'success') {
return submission.reply();
}
try {
// ログイン認証処理を実行
const user = await authenticator.authenticate('user-pass', request);
// ログイン認証処理に成功したら、クッキーにセッション情報を追加し、ログイン後ページへリダイレクト
const session = await sessionStorage.getSession(
request.headers.get('cookie')
);
session.set('user', user);
throw redirect('/', {
headers: { 'Set-Cookie': await sessionStorage.commitSession(session) },
});
} catch (error) {
if (error instanceof Error) {
// ログイン認証処理に失敗した場合は、エラーを追記して返す
return submission.reply({
formErrors: ['メールアドレス、またはパスワードが違います'],
});
}
// 上記以外の意図しないエラーの場合は、再送出する
throw error;
}
}
parseWithZod
関数を使うことで、フロント側でも定義したzodのフォームスキーマ定義を元に、送信データの検証をすることができます。もし、バリデーションエラーの場合は、サーバー側の検証結果を、フロント側に返答し、useActionData
経由で画面に結果を表示することができます。
そして、フォームバリデーションの検証に成功した場合は、ログイン処理を行います。
詳細は以下の別記事をご確認ください。
この時に注意点として、request
オブジェクトを複製(clone
)する必要がある点です。複製をしないと、ログイン処理の中でrequest
オブジェクト内のフォーム送信データが空オブジェクトとなってしまうからです。
ログイン認証処理に成功したら、クッキーにセッション情報を追加し、ログイン後ページへリダイレクトを行い、処理は終了となります。
ログイン認証失敗時のエラーハンドリング
最後に、ログイン認証処理に失敗した場合の処理を説明します。
今回の実装では、ログイン認証処理に失敗した場合は例外が送出されます。そのため、この場合はtry ~ catch
で例外処理を行います。
この時に、conform
では、submission.reply
処理時に、formErrors
に個別エラーメッセージを追加することができます。そのため、ログイン認証失敗時のエラーメッセージを付与して、フロント側に検証結果を返します。
return submission.reply({
formErrors: ['メールアドレス、またはパスワードが違います'],
});
そして、フロント側では、form.errors
から個別エラーメッセージを表示することができます。
{form.errors && (
<div>
{form.errors.map((error) => (
<FormMessage key={error}>{error}</FormMessage>
))}
</div>
)}
おわりに
以上が、今回実装したconform
を使った、Remixのログインフォーム画面の実装となります。
conform
を用いることで、フロント側とサーバー側のフォームバリデーションを簡単に実装することができました。
今後も、conform
を使った、フォームUIの実装を試していきたいと思います。
Discussion