14
章14
アクセシビリティの向上
前の章では、エラー(404エラーを含む)を捕捉し、ユーザーにフォールバックを表示する方法を見てきました。しかし、まだ議論する必要があるパズルがあります。それはフォームの検証です。サーバーアクションを使ったサーバーサイド検証の実装方法と、ReactのuseActionState
フックを使ってフォームエラーを表示する方法を見ていきましょう。アクセシビリティに配慮しながら!
この章の内容...
ここで取り上げるトピックは次のとおりです。
Next.jsでeslint-plugin-jsx-a11y
を使ってアクセシビリティのベストプラクティスを実装する方法。
サーバーサイドのフォーム検証を実装する方法。
ReactのuseActionState
フックを使ってフォームエラーを処理し、ユーザーに表示する方法。
アクセシビリティとは?
アクセシビリティとは、障害のある人を含め、誰もが使えるWebアプリケーションを設計し実装することを指します。キーボードナビゲーション、セマンティックHTML、画像、色、ビデオなど、多くの分野をカバーする広大なトピックです。
このコースではアクセシビリティについて深く掘り下げることはありませんが、Next.jsで利用できるアクセシビリティ機能と、アプリケーションのアクセシビリティを高めるための一般的なプラクティスについて説明します。
アクセシビリティについてもっと学びたい場合は、アクセシビリティを学ぶコース(web.dev)をお勧めします。
Next.jsでESLintアクセシビリティプラグインを使用する
Next.jsには、アクセシビリティの問題を早期に検出するのに役立つESLint設定にeslint-plugin-jsx-a11y
プラグインが含まれています。たとえば、このプラグインは、alt
テキストのない画像がある場合、aria-*
およびrole
属性を誤って使用している場合などに警告します。
オプションで、これを試してみたい場合は、package.json
ファイルにスクリプトとしてnext lint
を追加します。
"scripts": {
"build": "next build",
"dev": "next dev",
"start": "next start",
"lint": "next lint"
},
次に、ターミナルでpnpm lint
を実行します。
pnpm lint
これにより、プロジェクトのESLintのインストールと設定がガイドされます。ここでpnpm lint
を実行すると、次の出力が表示されるはずです。
✔ No ESLint warnings or errors
ただし、alt
テキストのない画像がある場合はどうなるでしょうか?調べてみましょう!
/app/ui/invoices/table.tsx
に移動し、画像からalt
プロパティを削除します。エディターの検索機能を使用すると、<Image>
をすばやく見つけることができます。
<Image
src={invoice.image_url}
className="rounded-full"
width={28}
height={28}
alt={`${invoice.name}'s profile picture`} // Delete this line
/>
ここでpnpm lint
を再度実行すると、次の警告が表示されるはずです。
./app/ui/invoices/table.tsx
45:25 Warning: Image elements must have an alt prop,
either with meaningful text, or an empty string for decorative images. jsx-a11y/alt-text
リンターの追加と設定は必須の手順ではありませんが、開発プロセスでアクセシビリティの問題を検出するのに役立つ場合があります。
フォームのアクセシビリティを向上させる
フォームのアクセシビリティを向上させるために、すでに3つのことを行っています。
- セマンティックHTML:
<div>
の代わりにセマンティック要素(<input>
、<option>
など)を使用します。これにより、支援技術(AT)が入力要素に焦点を当て、ユーザーに適切なコンテキスト情報を提供できるようになり、フォームのナビゲートと理解が容易になります。 - ラベル付け:
<label>
とhtmlFor
属性を含めることで、各フォームフィールドに説明的なテキストラベルが付けられます。これにより、コンテキストを提供することでATサポートが向上し、ユーザーがラベルをクリックして対応する入力フィールドにフォーカスできるようにすることで、使いやすさも向上します。 - フォーカスアウトライン:フィールドは、フォーカスがあるときにアウトラインが表示されるように適切にスタイルされています。これは、ページ上のアクティブな要素を視覚的に示すため、アクセシビリティにとって非常に重要であり、キーボードユーザーとスクリーンリーダーユーザーの両方がフォーム上のどこにいるかを理解するのに役立ちます。
tab
キーを押して確認できます。
これらのプラクティスは、多くのユーザーにとってフォームをよりアクセスしやすくするための優れた基盤を築きます。ただし、これらはフォーム検証とエラーに対処していません。
フォーム検証
http://localhost:3000/dashboard/invoices/createに移動し、空のフォームを送信します。どうなりますか?
エラーが発生します!これは、空のフォーム値をサーバーアクションに送信しているためです。これは、クライアントまたはサーバーでフォームを検証することで防ぐことができます。
クライアントサイドの検証
クライアントでフォームを検証する方法はいくつかあります。最も簡単な方法は、フォーム内の<input>
および<select>
要素にrequired
属性を追加して、ブラウザーが提供するフォーム検証を利用することです。例:
<input
id="amount"
name="amount"
type="number"
placeholder="Enter USD amount"
className="peer block w-full rounded-md border border-gray-200 py-2 pl-10 text-sm outline-2 placeholder:text-gray-500"
required
/>
フォームを再度送信します。空の値のフォームを送信しようとすると、ブラウザーに警告が表示されます。
一部のATはブラウザー検証をサポートしているため、このアプローチは一般的に問題ありません。
クライアントサイド検証の代替はサーバーサイド検証です。次のセクションで、どのように実装できるかを見てみましょう。今のところ、required
属性を追加した場合は削除してください。
サーバーサイド検証
サーバーでフォームを検証することで、次のことが可能になります。
- データをデータベースに送信する前に、データが予期された形式であることを確認します。
- 悪意のあるユーザーがクライアントサイドの検証をバイパスするリスクを減らします。
- 何が有効なデータと見なされるかについて、単一の信頼できるソースを持ちます。
create-form.tsx
コンポーネントで、react
からuseActionState
フックをインポートします。useActionState
はフックであるため、"use client"
ディレクティブを使用してフォームをクライアントコンポーネントにする必要があります。
'use client';
// ...
import { useActionState } from 'react';
フォームコンポーネント内では、useActionState
フックは
- 2つの引数
(action, initialState)
を取ります。 - 2つの値
[state, formAction]
(フォームの状態と、フォームが送信されたときに呼び出される関数)を返します。
useActionState
の引数としてcreateInvoice
アクションを渡し、<form action={}>
属性内でformAction
を呼び出します。
// ...
import { useActionState } from 'react';
export default function Form({ customers }: { customers: CustomerField[] }) {
const [state, formAction] = useActionState(createInvoice, initialState);
return <form action={formAction}>...</form>;
}
initialState
は、定義するものであれば何でもかまいません。この場合は、2つの空のキーmessage
とerrors
を持つオブジェクトを作成し、actions.ts
ファイルからState
型をインポートします。
// ...
import { createInvoice, State } from '@/app/lib/actions';
import { useActionState } from 'react';
export default function Form({ customers }: { customers: CustomerField[] }) {
const initialState: State = { message: null, errors: {} };
const [state, formAction] = useActionState(createInvoice, initialState);
return <form action={formAction}>...</form>;
}
最初は混乱するかもしれませんが、サーバーアクションを更新すると、より理解できるようになります。それでは、今すぐそれを実行しましょう。
action.ts
ファイルで、Zodを使用してフォームデータを検証できます。FormSchema
を次のように更新します。
const FormSchema = z.object({
id: z.string(),
customerId: z.string({
invalid_type_error: 'Please select a customer.',
}),
amount: z.coerce
.number()
.gt(0, { message: 'Please enter an amount greater than $0.' }),
status: z.enum(['pending', 'paid'], {
invalid_type_error: 'Please select an invoice status.',
}),
date: z.string(),
});
customerId
- Zodは、顧客フィールドがstring
型を予期しているため、空の場合、すでにエラーをスローします。ただし、ユーザーが顧客を選択しない場合に、フレンドリーなメッセージを追加してみましょう。amount
- 金額の型をstring
からnumber
に強制変換しているため、文字列が空の場合、デフォルトはゼロになります。.gt()
関数を使用して、金額を常に0より大きくする必要があることをZodに伝えましょう。status
- Zodは、ステータスフィールドが「pending」または「paid」を期待するため、空の場合には既にエラーをスローします。ユーザーがステータスを選択しない場合にも、分かりやすいメッセージを追加しましょう。
次に、createInvoice
アクションを、2つのパラメータ(prevState
とformData
)を受け入れるように更新します。
export type State = {
errors?: {
customerId?: string[];
amount?: string[];
status?: string[];
};
message?: string | null;
};
export async function createInvoice(prevState: State, formData: FormData) {
// ...
}
formData
- 以前と同じです。prevState
-useActionState
フックから渡された状態が含まれます。この例ではアクション内で使用しませんが、必須のプロパティです。
次に、Zodのparse()
関数をsafeParse()
に変更します。
export async function createInvoice(prevState: State, formData: FormData) {
// Validate form fields using Zod
const validatedFields = CreateInvoice.safeParse({
customerId: formData.get('customerId'),
amount: formData.get('amount'),
status: formData.get('status'),
});
// ...
}
safeParse()
は、success
またはerror
フィールドのいずれかを含むオブジェクトを返します。これにより、try/catch
ブロック内にこのロジックを記述することなく、検証をより優雅に処理できます。
情報をデータベースに送信する前に、条件付きでフォームフィールドが正しく検証されたかどうかを確認します。
export async function createInvoice(prevState: State, formData: FormData) {
// Validate form fields using Zod
const validatedFields = CreateInvoice.safeParse({
customerId: formData.get('customerId'),
amount: formData.get('amount'),
status: formData.get('status'),
});
// If form validation fails, return errors early. Otherwise, continue.
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
message: 'Missing Fields. Failed to Create Invoice.',
};
}
// ...
}
validatedFields
が成功しなかった場合、Zodからのエラーメッセージとともに、関数を早期に返します。
ヒント:
validatedFields
をconsole.logで出力し、空のフォームを送信してその形状を確認してください。
最後に、フォームの検証をtry/catchブロックの外で個別に処理しているため、データベースエラーに対して特定のエラーメッセージを返すことができます。最終的なコードは次のようになります。
export async function createInvoice(prevState: State, formData: FormData) {
// Validate form using Zod
const validatedFields = CreateInvoice.safeParse({
customerId: formData.get('customerId'),
amount: formData.get('amount'),
status: formData.get('status'),
});
// If form validation fails, return errors early. Otherwise, continue.
if (!validatedFields.success) {
return {
errors: validatedFields.error.flatten().fieldErrors,
message: 'Missing Fields. Failed to Create Invoice.',
};
}
// Prepare data for insertion into the database
const { customerId, amount, status } = validatedFields.data;
const amountInCents = amount * 100;
const date = new Date().toISOString().split('T')[0];
// Insert data into the database
try {
await sql`
INSERT INTO invoices (customer_id, amount, status, date)
VALUES (${customerId}, ${amountInCents}, ${status}, ${date})
`;
} catch (error) {
// If a database error occurs, return a more specific error.
return {
message: 'Database Error: Failed to Create Invoice.',
};
}
// Revalidate the cache for the invoices page and redirect the user.
revalidatePath('/dashboard/invoices');
redirect('/dashboard/invoices');
}
素晴らしい、これでフォームコンポーネントにエラーを表示しましょう。create-form.tsx
コンポーネントに戻り、フォームのstate
を使用してエラーにアクセスできます。
各特定のエラーをチェックする三項演算子を追加します。たとえば、顧客のフィールドの後に、次を追加できます。
<form action={formAction}>
<div className="rounded-md bg-gray-50 p-4 md:p-6">
{/* Customer Name */}
<div className="mb-4">
<label htmlFor="customer" className="mb-2 block text-sm font-medium">
Choose customer
</label>
<div className="relative">
<select
id="customer"
name="customerId"
className="peer block w-full rounded-md border border-gray-200 py-2 pl-10 text-sm outline-2 placeholder:text-gray-500"
defaultValue=""
aria-describedby="customer-error"
>
<option value="" disabled>
Select a customer
</option>
{customers.map((name) => (
<option key={name.id} value={name.id}>
{name.name}
</option>
))}
</select>
<UserCircleIcon className="pointer-events-none absolute left-3 top-1/2 h-[18px] w-[18px] -translate-y-1/2 text-gray-500" />
</div>
<div id="customer-error" aria-live="polite" aria-atomic="true">
{state.errors?.customerId &&
state.errors.customerId.map((error: string) => (
<p className="mt-2 text-sm text-red-500" key={error}>
{error}
</p>
))}
</div>
</div>
// ...
</div>
</form>
ヒント: コンポーネント内で
state
をconsole.logで出力し、すべてが正しく接続されているか確認できます。フォームがクライアントコンポーネントになったので、開発ツールでコンソールを確認してください。
上記のコードでは、次のariaラベルも追加しています。
aria-describedby="customer-error"
:これは、select
要素とエラーメッセージコンテナ間の関係を確立します。これは、id="customer-error"
を持つコンテナがselect
要素を説明することを示します。スクリーンリーダーは、ユーザーがselect
ボックスを操作するときに、この説明を読み上げてエラーを通知します。id="customer-error"
:このid
属性は、select
入力のエラーメッセージを保持するHTML要素を一意に識別します。これは、aria-describedby
が関係を確立するために必要です。aria-live="polite"
:スクリーンリーダーは、div
内のエラーが更新されたときに、ユーザーに丁寧に通知する必要があります。コンテンツが変更された場合(たとえば、ユーザーがエラーを修正した場合)、スクリーンリーダーはこれらの変更をアナウンスしますが、ユーザーが中断されないように、アイドル状態の場合のみです。
練習:ariaラベルの追加
上記の例を使用して、残りのフォームフィールドにエラーを追加します。フィールドが不足している場合は、フォームの下部にメッセージを表示する必要があります。UIは次のようになります。


準備ができたら、pnpm lint
を実行して、ariaラベルを正しく使用しているかどうかを確認します。
挑戦したい場合は、この章で学んだ知識を活用して、edit-form.tsx
コンポーネントにフォーム検証を追加してください。
次のことを行う必要があります。
useActionState
をedit-form.tsx
コンポーネントに追加します。- Zodからの検証エラーを処理するように
updateInvoice
アクションを編集します。 - コンポーネントにエラーを表示し、アクセシビリティを向上させるためにariaラベルを追加します。
準備ができたら、以下のコードスニペットを展開して解決策を確認してください。
章を完了しました14
素晴らしい。React Form Statusとサーバーサイド検証を使用して、フォームのアクセシビリティを向上させる方法を学びました。
これは役に立ちましたか?