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 テスト方針

優先順位:

  1. ユーザーから見える振る舞い
  2. アクセシブルな問い合わせ
  3. 統合テスト
  4. 必要な単体テスト
  5. 主要導線の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生成には使わない。