Reactでログインページを作る:フォームから保護されたページまで

Reactでログインページを作る:フォームから保護されたページまで

ログインページはほぼすべてのWebサイトにある基本的な部品です。この記事では Reactでフォームの構造、制御された入力、入力チェック、ログインの模擬、連続失敗後のロック、Contextによるユーザー状態の共有、そしてサインインしていない人に内容を見せないページまでを段階的に作ります。すべての例は実行済みで、表示している出力は実際の結果です。

必要なもの

Node.js 18以降とReactの基礎が必要です。例はReact 18.3.1を使い、この記事のテストランナー(jsdomでブラウザ上のクリックを再現)で実行します。注意:ブラウザ内でパスワードを確認するのは学習用の例に過ぎません。実際のシステムはサーバーで確認し、パスワードはハッシュ化して保存し、HTTPSを使ってください。

図解

サインインの流れ:入力、チェック、サーバーへの送信、状態の保存、ページの保護
サインインの流れ:入力、チェック、サーバーへの送信、状態の保存、ページの保護

例 1: ログインフォームの構造

メールの入力欄、パスワードの入力欄、送信ボタンを持つフォームから始めます。renderToStaticMarkupでReactが生成するHTMLを確認します。

import React from "react";

export function App() {
  return (
    <form>
      <h2>เข้าสู่ระบบ</h2>
      <label>อีเมล<input type="email" name="email" /></label>
      <label>รหัสผ่าน<input type="password" name="password" /></label>
      <button type="submit">เข้าสู่ระบบ</button>
    </form>
  );
}

実際の出力

<form><h2>เข้าสู่ระบบ</h2><label>อีเมล<input type="email" name="email"/></label><label>รหัสผ่าน<input type="password" name="password"/></label><button type="submit">เข้าสู่ระบบ</button></form>

例 2: パスワードの表示と非表示

表示状態をstateに保存し、ボタンを押すたびにinputの種類をpasswordとtextで切り替えます。

1. 表示ボタンを1回押す(オフからオンへ)

import React, { useState } from "react";

export function App() {
  const [show, setShow] = useState(false);
  return (
    <div>
      <input type={show ? "text" : "password"} value="secret1" readOnly />
      <button id="toggle" onClick={() => setShow(!show)}>แสดงรหัสผ่าน</button>
      <p data-out>แสดงรหัสผ่าน: {show ? "เปิด" : "ปิด"}</p>
    </div>
  );
}

export const steps = ["#toggle"];

実際の出力

ข้อความเริ่มต้น: แสดงรหัสผ่าน: ปิด
หลังคลิกครั้งที่ 1: แสดงรหัสผ่าน: เปิด

例 3: 入力内容のチェック

validate関数はエラーメッセージの一覧を返します。ユーザーが送信を押した後にだけ表示し、入力中に騒がしくならないようにします。

import React, { useState } from "react";

// กติกาการตรวจสอบ: คืนรายการข้อความแจ้งเตือน ถ้าไม่มีข้อผิดพลาดจะได้อาร์เรย์ว่าง
export function validate(email, password) {
  const errors = [];
  if (!email) errors.push("กรอกอีเมล");
  else if (!/^\S+@\S+\.\S+$/.test(email)) errors.push("รูปแบบอีเมลไม่ถูกต้อง");
  if (password.length < 6) errors.push("รหัสผ่านอย่างน้อย 6 ตัว");
  return errors;
}

export function App() {
  const [email, setEmail] = useState("");
  const [password, setPassword] = useState("");
  const [submitted, setSubmitted] = useState(false);
  const errors = validate(email, password);
  return (
    <div>
      <input value={email} onChange={e => setEmail(e.target.value)} />
      <input value={password} onChange={e => setPassword(e.target.value)} />
      <button id="submit" onClick={() => setSubmitted(true)}>ส่งฟอร์ม</button>
      <p data-out>{!submitted ? "ยังไม่ส่งฟอร์ม" : errors.length ? errors.join(" | ") : "ไม่มีข้อผิดพลาด"}</p>
    </div>
  );
}

export const steps = ["#submit"];

実際の出力

ข้อความเริ่มต้น: ยังไม่ส่งฟอร์ม
หลังคลิกครั้งที่ 1: กรอกอีเมล | รหัสผ่านอย่างน้อย 6 ตัว

例 4: ログインの模擬

メールとパスワードをサンプルデータと比べ、成功またはアカウントが見つからないメッセージを表示します。この例は正しい値で始まります。

import React, { useState } from "react";

// ตัวอย่างฐานข้อมูลผู้ใช้จำลอง จริง ๆ ต้องตรวจที่เซิร์ฟเวอร์เท่านั้น
const USERS = { "[email protected]": "secret1" };

export function login(email, password) {
  if (USERS[email] === undefined) return { ok: false, message: "ไม่พบบัญชีนี้" };
  if (USERS[email] !== password) return { ok: false, message: "รหัสผ่านไม่ถูกต้อง" };
  return { ok: true, message: "เข้าสู่ระบบสำเร็จ: " + email };
}

export function App() {
  const [email, setEmail] = useState("[email protected]");
  const [password, setPassword] = useState("secret1");
  const [result, setResult] = useState("ยังไม่ได้ลองเข้าสู่ระบบ");
  return (
    <div>
      <input value={email} onChange={e => setEmail(e.target.value)} />
      <input value={password} onChange={e => setPassword(e.target.value)} />
      <button id="submit" onClick={() => setResult(login(email, password).message)}>เข้าสู่ระบบ</button>
      <p data-out>{result}</p>
    </div>
  );
}

export const steps = ["#submit"];

実際の出力

ข้อความเริ่มต้น: ยังไม่ได้ลองเข้าสู่ระบบ
หลังคลิกครั้งที่ 1: เข้าสู่ระบบสำเร็จ: [email protected]

例 5: 失敗が続いたらロック

パスワードの誤りを数え、上限に達したらボタンを無効にして、繰り返しの推測を防ぎます。

import React, { useState } from "react";

const MAX_TRIES = 3;

export function App() {
  const [password, setPassword] = useState("wrong");
  const [tries, setTries] = useState(0);
  const [message, setMessage] = useState("");
  const locked = tries >= MAX_TRIES;

  function submit() {
    if (locked) return;
    if (password === "secret1") {
      setMessage("เข้าสู่ระบบสำเร็จ");
      return;
    }
    const next = tries + 1;
    setTries(next);
    setMessage(next >= MAX_TRIES ? "ล็อกชั่วคราว ลองใหม่ภายหลัง" : `รหัสผ่านไม่ถูกต้อง (ครั้งที่ ${next} จาก ${MAX_TRIES})`);
  }

  return (
    <div>
      <input value={password} onChange={e => setPassword(e.target.value)} />
      <button id="submit" disabled={locked} onClick={submit}>เข้าสู่ระบบ</button>
      <p data-out>{message || "พร้อมใช้งาน"}</p>
    </div>
  );
}

export const steps = ["#submit", "#submit", "#submit"];

実際の出力

ข้อความเริ่มต้น: พร้อมใช้งาน
หลังคลิกครั้งที่ 1: รหัสผ่านไม่ถูกต้อง (ครั้งที่ 1 จาก 3)
หลังคลิกครั้งที่ 2: รหัสผ่านไม่ถูกต้อง (ครั้งที่ 2 จาก 3)
หลังคลิกครั้งที่ 3: ล็อกชั่วคราว ลองใหม่ภายหลัง

例 6: ログイン状態を保持するオプション

stateで制御するチェックボックスで、状態をすぐに表示します。

import React, { useState } from "react";

export function App() {
  const [remember, setRemember] = useState(false);
  return (
    <div>
      <label>
        <input id="remember" type="checkbox" checked={remember} onChange={e => setRemember(e.target.checked)} />
        จดจำการเข้าสู่ระบบ
      </label>
      <p data-out>จดจำการเข้าสู่ระบบ: {remember ? "เปิด" : "ปิด"}</p>
    </div>
  );
}

export const steps = ["#remember"];

実際の出力

ข้อความเริ่มต้น: จดจำการเข้าสู่ระบบ: ปิด
หลังคลิกครั้งที่ 1: จดจำการเข้าสู่ระบบ: เปิด

例 7: Contextでユーザーの状態を共有する

AuthContextを作り、ステータスバーとボタンがpropsを何段も渡さずにユーザー情報へアクセスできるようにします。

import React, { createContext, useContext, useState } from "react";

// เก็บสถานะผู้ใช้ไว้ที่ Context เพื่อให้ทุกส่วนของหน้าเข้าถึงได้โดยไม่ต้องส่ง props ต่อกันหลายชั้น
const AuthContext = createContext(null);

function StatusBar() {
  const { user, login, logout } = useContext(AuthContext);
  return (
    <div>
      <p data-out>สถานะ: {user ? "สวัสดี " + user : "ยังไม่ได้เข้าสู่ระบบ"}</p>
      {user
        ? <button id="logout" onClick={logout}>ออกจากระบบ</button>
        : <button id="login" onClick={() => login("สมชาย")}>เข้าสู่ระบบ</button>}
    </div>
  );
}

export function App() {
  const [user, setUser] = useState(null);
  const value = { user, login: setUser, logout: () => setUser(null) };
  return (
    <AuthContext.Provider value={value}>
      <StatusBar />
    </AuthContext.Provider>
  );
}

export const steps = ["#login", "#logout"];

実際の出力

ข้อความเริ่มต้น: สถานะ: ยังไม่ได้เข้าสู่ระบบ
หลังคลิกครั้งที่ 1: สถานะ: สวัสดี สมชาย
หลังคลิกครั้งที่ 2: สถานะ: ยังไม่ได้เข้าสู่ระบบ

例 8: サインインが必要なページ

ユーザーがいなければサインインを促し、いればダッシュボードを表示します。これが保護されたルートの考え方です。

1. サインインしてからサインアウトする

import React, { useState } from "react";

// หน้าที่ต้องเข้าสู่ระบบก่อนจึงจะเห็น ถ้ายังไม่มีผู้ใช้จะแสดงข้อความให้เข้าสู่ระบบแทน
function Dashboard({ user }) {
  if (!user) return <p data-out>กรุณาเข้าสู่ระบบก่อน</p>;
  return <p data-out>หน้าแดชบอร์ดของ {user}</p>;
}

export function App() {
  const [user, setUser] = useState(null);
  return (
    <div>
      <button id="login" onClick={() => setUser("สมชาย")}>เข้าสู่ระบบ</button>
      <button id="logout" onClick={() => setUser(null)}>ออกจากระบบ</button>
      <Dashboard user={user} />
    </div>
  );
}

export const steps = ["#login", "#logout"];

実際の出力

ข้อความเริ่มต้น: กรุณาเข้าสู่ระบบก่อน
หลังคลิกครั้งที่ 1: หน้าแดชบอร์ดของ สมชาย
หลังคลิกครั้งที่ 2: กรุณาเข้าสู่ระบบก่อน

例 9: 完成したログインページ

フォーム、適切なautocompleteの入力欄、保存オプション、パスワードを忘れた場合のリンクを組み合わせた最終ページです。

import React from "react";

// หน้าเข้าสู่ระบบแบบสมบูรณ์ที่รวมทุกส่วนไว้ด้วยกัน: ฟอร์ม ช่องกรอก ตัวเลือกจดจำ และลิงก์ลืมรหัสผ่าน
export function App() {
  return (
    <main>
      <h1>เข้าสู่ระบบ</h1>
      <form>
        <label>อีเมล<input type="email" name="email" autoComplete="username" /></label>
        <label>รหัสผ่าน<input type="password" name="password" autoComplete="current-password" /></label>
        <label><input type="checkbox" name="remember" /> จดจำการเข้าสู่ระบบ</label>
        <button type="submit">เข้าสู่ระบบ</button>
      </form>
      <a href="/forgot">ลืมรหัสผ่าน?</a>
    </main>
  );
}

実際の出力

<main><h1>เข้าสู่ระบบ</h1><form><label>อีเมล<input type="email" name="email" autoComplete="username"/></label><label>รหัสผ่าน<input type="password" name="password" autoComplete="current-password"/></label><label><input type="checkbox" name="remember"/> จดจำการเข้าสู่ระบบ</label><button type="submit">เข้าสู่ระบบ</button></form><a href="/forgot">ลืมรหัสผ่าน?</a></main>

次のステップ

次は、ログインページをfetchで実際のAPIに接続し、HTTP-only Cookieや安全なトークンを使い、メールで再設定リンクを送るパスワード再設定を追加しましょう。

まとめ

よいログインページは、stateで制御されたフォーム、入力チェック、失敗回数の制限、Contextによるユーザー状態の共有、サインインを確認してからデータを見せるページで構成されます。実際のシステムでは、アクセス権の確認を必ずサーバーで行ってください。