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によるユーザー状態の共有、サインインを確認してからデータを見せるページで構成されます。実際のシステムでは、アクセス権の確認を必ずサーバーで行ってください。