19.1 TypeScript
DOM propsの継承
type ButtonProps = {
variant?: "primary" | "secondary";
} & React.ComponentPropsWithoutRef<"button">;
function Button({ variant = "primary", ...props }: ButtonProps) {
return <button data-variant={variant} {...props} />;
}
判別可能Union
type AsyncState<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: Error };
booleanを複数並べるより、不可能な状態を型で排除できる。
イベント型
function handleChange(
event: React.ChangeEvent<HTMLInputElement>
) {
setValue(event.target.value);
}
19.2 テスト方針
優先順位:
- ユーザーから見える振る舞い
- アクセシブルな問い合わせ
- 統合テスト
- 必要な単体テスト
- 主要導線のE2E
実装詳細への依存を避ける。
render(<LoginForm />);
await user.type(
screen.getByLabelText("メールアドレス"),
"test@example.com"
);
await user.click(
screen.getByRole("button", { name: "ログイン" })
);
expect(
await screen.findByText("ログインしました")
).toBeInTheDocument();
19.3 act
React更新をテスト環境で完了させる境界。Testing Libraryの高レベルAPIは多くの場合actを内部で扱う。警告を抑えるためだけに乱用せず、未完了の非同期更新を確認する。
19.4 E2E
対象:
- ログイン
- 決済
- 登録
- 権限
- 主要検索
- 重要なCRUD
- 外部サービス連携
全組み合わせをE2Eにせず、テストピラミッドと障害影響で選ぶ。
19.5 アクセシビリティ
セマンティックHTML
<button type="button" onClick={openDialog}>
設定を開く
</button>
クリック可能なdivではなくbuttonを使う。
ラベル
<label htmlFor="email">メールアドレス</label>
<input id="email" name="email" type="email" />
エラー
<input
aria-invalid={Boolean(error)}
aria-describedby={error ? "email-error" : undefined}
/>
{error && (
<p id="email-error" role="alert">
{error}
</p>
)}
動的UI
モーダル、タブ、コンボボックス、メニューはキーボード操作・フォーカス管理・ARIA仕様が複雑である。実績あるアクセシブルなプリミティブの利用を検討する。
19.6 useId
SSRとhydrationで安定したIDを生成する。
function PasswordField() {
const hintId = useId();
return (
<>
<label>
パスワード
<input
type="password"
aria-describedby={hintId}
/>
</label>
<p id={hintId}>12文字以上で入力してください。</p>
</>
);
}
リストのkey生成には使わない。