Reactで、モックのjsonを使って、ログイン画面を作る練習をします
パスワードは固定でパスワードです
どのように作ればいいか
画面はコンポーネント使います
**Reactで「モックJSONを使ったログイン画面」**は、Reactの基本(コンポーネント分割 / state / イベント処理 / フォーム制御 / 条件分岐)が全部入っていてとても良い題材です。
ユーザーさんは段階的に作る説明のほうが合うので、実務っぽい構成で進めます。
⸻
作るもの
仕様を整理します。
• ログイン画面
• ユーザー一覧は mock JSON
• パスワードは全員固定で "password"
• メールアドレス or ユーザー名でログイン
• Reactコンポーネントで分割
• ログイン成功 → Welcome画面
• ログイン失敗 → エラーメッセージ
例:
ユーザー名: test
パスワード: password
→ ログイン成功
⸻
ディレクトリ構成
初心者向けで分かりやすくします。
src/
├─ components/
│ ├─ LoginForm.jsx
│ ├─ InputField.jsx
│ ├─ Button.jsx
│ └─ Welcome.jsx
│
├─ data/
│ └─ users.json
│
├─ App.jsx
└─ main.jsx
実務でも近い構成です。
⸻
- モックJSON作成
src/data/users.json
[
{
"id": 1,
"username": "test",
"email": "test@test.com",
"name": "テスト"
},
{
"id": 2,
"username": "tanaka",
"email": "tanaka@test.com",
"name": "田中"
},
{
"id": 3,
"username": "suzuki",
"email": "suzuki@test.com",
"name": "鈴木"
}
]
⸻
- コンポーネント設計
役割分担します。
コンポーネント 役割
App 全体管理
LoginForm ログインフォーム
InputField 入力欄
Button ボタン
Welcome 成功画面
⸻
- InputField
共通入力パーツ
components/InputField.jsx
function InputField({ label, type, value, onChange }) {
return (
<div>
<label>{label}</label>
<input
type={type}
value={value}
onChange={onChange}
/>
</div>
);
}
export default InputField;
⸻
- Button
components/Button.jsx
function Button({ text, onClick }) {
return (
<button onClick={onClick}>
{text}
</button>
);
}
export default Button;
⸻
- Welcome
components/Welcome.jsx
function Welcome({ user }) {
return (
<div>
<h2>ようこそ {user.name} さん</h2>
</div>
);
}
export default Welcome;
⸻
- LoginForm
ここがメインです。
components/LoginForm.jsx
import { useState } from "react";
import users from "../data/users.json";
import InputField from "./InputField";
import Button from "./Button";
function LoginForm({ onLogin }) {
const [username, setUsername] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const handleLogin = () => {
const user = users.find(
(u) =>
(u.username === username || u.email === username) &&
password === "password"
);
if (user) {
onLogin(user);
setError("");
} else {
setError("ログインに失敗しました");
}
};
return (
<div>
<h1>ログイン</h1>
<InputField
label="ユーザー名 or メール"
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
/>
<InputField
label="パスワード"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<Button text="ログイン" onClick={handleLogin} />
{error && <p>{error}</p>}
</div>
);
}
export default LoginForm;
⸻
- App
全体管理
App.jsx
import { useState } from "react";
import LoginForm from "./components/LoginForm";
import Welcome from "./components/Welcome";
function App() {
const [loginUser, setLoginUser] = useState(null);
return (
<div>
{loginUser ? (
<Welcome user={loginUser} />
) : (
<LoginForm onLogin={setLoginUser} />
)}
</div>
);
}
export default App;
⸻
Reactで学べること
この練習でかなり重要なものが入っています。
useState
const [username, setUsername] = useState("");
状態管理
⸻
props
<InputField label="パスワード" />
親 → 子へ値を渡す
⸻
イベント処理
onClick={handleLogin}
⸻
条件レンダリング
loginUser ? <Welcome /> : <LoginForm />
TypeScriptなら、こういう構成がきれいです。
src/
├─ components/
│ ├─ LoginForm.tsx
│ ├─ InputField.tsx
│ ├─ Button.tsx
│ └─ Welcome.tsx
├─ data/
│ └─ users.json
├─ types/
│ └─ user.ts
└─ App.tsx
1. ユーザー型
src/types/user.ts
```php3
export type User = {
id: number;
username: string;
email: string;
name: string;
};
- users.json
src/data/users.json
[
{
"id": 1,
"username": "test",
"email": "test@test.com",
"name": "テスト"
},
{
"id": 2,
"username": "tanaka",
"email": "tanaka@test.com",
"name": "田中"
}
]
- InputField.tsx
type InputFieldProps = {
label: string;
type: "text" | "password" | "email";
value: string;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
};
function InputField({ label, type, value, onChange }: InputFieldProps) {
return (
<div>
<label>{label}</label>
<input type={type} value={value} onChange={onChange} />
</div>
);
}
export default InputField;
- Button.tsx
type ButtonProps = {
text: string;
onClick: () => void;
};
function Button({ text, onClick }: ButtonProps) {
return <button onClick={onClick}>{text}</button>;
}
export default Button;
- Welcome.tsx
import type { User } from "../types/user";
type WelcomeProps = {
user: User;
};
function Welcome({ user }: WelcomeProps) {
return <h2>ようこそ {user.name} さん</h2>;
}
export default Welcome;
- LoginForm.tsx
import { useState } from "react";
import usersData from "../data/users.json";
import type { User } from "../types/user";
import InputField from "./InputField";
import Button from "./Button";
const users: User[] = usersData;
type LoginFormProps = {
onLogin: (user: User) => void;
};
function LoginForm({ onLogin }: LoginFormProps) {
const [loginId, setLoginId] = useState<string>("");
const [password, setPassword] = useState<string>("");
const [error, setError] = useState<string>("");
const handleLogin = () => {
const user = users.find((user) => {
return (
(user.username === loginId || user.email === loginId) &&
password === "password"
);
});
if (!user) {
setError("ユーザー名またはパスワードが違います");
return;
}
setError("");
onLogin(user);
};
return (
<div>
<h1>ログイン</h1>
<InputField
label="ユーザー名 または メールアドレス"
type="text"
value={loginId}
onChange={(e) => setLoginId(e.target.value)}
/>
<InputField
label="パスワード"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<Button text="ログイン" onClick={handleLogin} />
{error && <p>{error}</p>}
</div>
);
}
export default LoginForm;
- App.tsx
import { useState } from "react";
import type { User } from "./types/user";
import LoginForm from "./components/LoginForm";
import Welcome from "./components/Welcome";
function App() {
const [loginUser, setLoginUser] = useState<User | null>(null);
return (
<div>
{loginUser ? (
<Welcome user={loginUser} />
) : (
<LoginForm onLogin={setLoginUser} />
)}
</div>
);
}
export default App;
ポイントはここです。
const [loginUser, setLoginUser] = useState<User | null>(null);
最初はログインしていないので null。
ログイン後は User 型のデータが入ります。
type LoginFormProps = {
onLogin: (user: User) => void;
};
これは「LoginFormから親のAppへ、ログイン成功したユーザーを渡す」という意味です。
このブログの続きで、
機能を追加したい
ログインしてるかどうかの管理
App.tsxからrouteでログインしていないといけないページとそうでないページに分ける
jsとtypescriptでそれぞれ作ってみて
React Router は と で画面を分け、子ルートは に表示できます。ログインしていない場合の遷移には を使う形が分かりやすいです。 
追加する内容
npm install react-router-dom
⸻
JavaScript版
構成
src/
├─ App.jsx
├─ components/
│ ├─ LoginForm.jsx
│ ├─ Welcome.jsx
│ └─ RequireAuth.jsx
├─ pages/
│ ├─ Home.jsx
│ ├─ Login.jsx
│ └─ MyPage.jsx
└─ data/
└─ users.json
RequireAuth.jsx
import { Navigate, Outlet } from "react-router-dom";
function RequireAuth({ loginUser }) {
if (!loginUser) {
return <Navigate to="/login" replace />;
}
return <Outlet />;
}
export default RequireAuth;
App.jsx
import { useState } from "react";
import { BrowserRouter, Routes, Route, Link } from "react-router-dom";
import RequireAuth from "./components/RequireAuth";
import Home from "./pages/Home";
import Login from "./pages/Login";
import MyPage from "./pages/MyPage";
function App() {
const [loginUser, setLoginUser] = useState(null);
const handleLogout = () => {
setLoginUser(null);
};
return (
<BrowserRouter>
<nav>
<Link to="/">ホーム</Link> |{" "}
<Link to="/mypage">マイページ</Link> |{" "}
<Link to="/login">ログイン</Link>
</nav>
{loginUser && (
<p>
{loginUser.name} さんでログイン中
<button onClick={handleLogout}>ログアウト</button>
</p>
)}
<Routes>
<Route path="/" element={<Home />} />
<Route
path="/login"
element={<Login onLogin={setLoginUser} />}
/>
<Route element={<RequireAuth loginUser={loginUser} />}>
<Route
path="/mypage"
element={<MyPage loginUser={loginUser} />}
/>
</Route>
</Routes>
</BrowserRouter>
);
}
export default App;
pages/Login.jsx
import LoginForm from "../components/LoginForm";
function Login({ onLogin }) {
return <LoginForm onLogin={onLogin} />;
}
export default Login;
pages/MyPage.jsx
function MyPage({ loginUser }) {
return (
<div>
<h1>マイページ</h1>
<p>{loginUser.name} さん専用ページです。</p>
</div>
);
}
export default MyPage;
⸻
TypeScript版
RequireAuth.tsx
import { Navigate, Outlet } from "react-router-dom";
import type { User } from "../types/user";
type RequireAuthProps = {
loginUser: User | null;
};
function RequireAuth({ loginUser }: RequireAuthProps) {
if (!loginUser) {
return <Navigate to="/login" replace />;
}
return <Outlet />;
}
export default RequireAuth;
App.tsx
import { useState } from "react";
import { BrowserRouter, Routes, Route, Link } from "react-router-dom";
import type { User } from "./types/user";
import RequireAuth from "./components/RequireAuth";
import Home from "./pages/Home";
import Login from "./pages/Login";
import MyPage from "./pages/MyPage";
function App() {
const [loginUser, setLoginUser] = useState<User | null>(null);
const handleLogout = () => {
setLoginUser(null);
};
return (
<BrowserRouter>
<nav>
<Link to="/">ホーム</Link> |{" "}
<Link to="/mypage">マイページ</Link> |{" "}
<Link to="/login">ログイン</Link>
</nav>
{loginUser && (
<p>
{loginUser.name} さんでログイン中
<button onClick={handleLogout}>ログアウト</button>
</p>
)}
<Routes>
<Route path="/" element={<Home />} />
<Route
path="/login"
element={<Login onLogin={setLoginUser} />}
/>
<Route element={<RequireAuth loginUser={loginUser} />}>
<Route
path="/mypage"
element={<MyPage loginUser={loginUser} />}
/>
</Route>
</Routes>
</BrowserRouter>
);
}
export default App;
pages/Login.tsx
import LoginForm from "../components/LoginForm";
import type { User } from "../types/user";
type LoginProps = {
onLogin: (user: User) => void;
};
function Login({ onLogin }: LoginProps) {
return <LoginForm onLogin={onLogin} />;
}
export default Login;
pages/MyPage.tsx
import type { User } from "../types/user";
type MyPageProps = {
loginUser: User;
};
function MyPage({ loginUser }: MyPageProps) {
return (
<div>
<h1>マイページ</h1>
<p>{loginUser.name} さん専用ページです。</p>
</div>
);
}
export default MyPage;
記事に書くポイント
loginUser が null のときはログインしていない状態です。
const [loginUser, setLoginUser] = useState(null);
ログイン必須ページはこの中に入れます。
<Route element={<RequireAuth loginUser={loginUser} />}>
<Route path="/mypage" element={<MyPage loginUser={loginUser} />} />
</Route>
未ログインなら /login に戻します。
return <Navigate to="/login" replace />;
今回は「画面を分ける」「ログインしているか判定する」「未ログインならログイン画面に戻す」練習です。
保存はどこにされているか
すごく良い質問です。ここが React の理解ポイントです。
const [user, setUser] = useState(null);
これ、どこかのファイルに保存されているわけではありません。
まず結論:
React がブラウザのメモリ(RAM)上に保持しています。
⸻
どこにあるの?
例えば
function AuthProvider({ children }) {
const [user, setUser] = useState(null);
}
この user は
AuthProvider コンポーネント専用の React の状態(state)
として保持されます。
イメージ:
ブラウザ
└ React
└ AuthProvider
└ state
└ user = null
ログインすると
setUser({
id: 1,
name: "山田"
});
↓
ブラウザ
└ React
└ AuthProvider
└ state
└ user = { id:1, name:"山田" }
⸻
どのタイミングで消える?
ブラウザを更新すると消えます。
なぜ?
メモリ上だからです。
F5
↓
Reactアプリ再起動
↓
useState(null) が最初から実行
↓
user = null
つまり
ログイン状態が消える
⸻
PHPの session と違う
PHPだと
$_SESSION['user'] = 'tanaka';
これはサーバー側に保存されます。
でも React の
useState()
は
クライアント(ブラウザ)だけ
です。
⸻
localStorage との違い
もし
localStorage.setItem("user", JSON.stringify(user));
なら、
ブラウザの保存領域に残ります。
ブラウザ
├ メモリ(useState)
└ localStorage(永続)
違い:
useState
• 高速
• Reactが管理
• 画面更新で消える
⸻
localStorage
• リロードしても残る
• 自分で読み書きする
• 文字列しか保存できない
⸻
実際のログインは?
本物のログインではこうです。
ログインフォーム
↓
APIへ送信
↓
サーバーで認証
↓
token を返す
↓
localStorage or cookie に保存
↓
React state に反映
例えば:
const login = async () => {
const res = await fetch("/api/login");
const data = await res.json();
localStorage.setItem("token", data.token);
setUser(data.user);
};
⸻
Context に入れてるのは何?
今回の
は
「保存」ではなく
共有しているだけ
です。
Context = 配達係
AuthProvider
user を持ってる
↓
Context が配る
↓
Header
Profile
ProtectedRoute
実際に持ってるのは
useState
です。
⸻
まとめ
const [user, setUser] = useState(null);
保存先:
React がブラウザメモリに保持
特徴:
• ファイルには保存されない
• DBにも保存されない
• リロードで消える
• Contextは共有するだけ
⸻
ここを理解すると次の疑問が出ます。
「じゃあリロードしてもログイン維持するには?」
これは localStorage / sessionStorage / cookie / JWT
の話になります。
もうちょっと詳しく
⸻
作るもの
未ログインでも見られるページ
- /
- /login
- /about
ログインしていないと見られないページ
- /mypage
- /dashboard
ログイン状態はここで管理します。
src/context/AuthContext.tsx
⸻
完成構成
src/
├ data/
│ └ users.json
├ types/
│ └ user.ts
├ context/
│ └ AuthContext.tsx
├ components/
│ ├ Header.tsx
│ └ ProtectedRoute.tsx
├ pages/
│ ├ HomePage.tsx
│ ├ LoginPage.tsx
│ ├ AboutPage.tsx
│ ├ MyPage.tsx
│ └ DashboardPage.tsx
├ App.tsx
└ main.tsx
⸻
- users.json
[
{
"id": 1,
"username": "ando",
"email": "ando@test.com",
"name": "安藤"
},
{
"id": 2,
"username": "tanaka",
"email": "tanaka@test.com",
"name": "田中"
}
]
パスワードは全員共通で password にします。
⸻
- types/user.ts
export type User = {
id: number;
username: string;
email: string;
name: string;
};
⸻
- context/AuthContext.tsx
import { createContext, useContext, useState } from "react";
import usersData from "../data/users.json";
import type { User } from "../types/user";
type AuthContextType = {
user: User | null;
isLoggedIn: boolean;
login: (loginId: string, password: string) => boolean;
logout: () => void;
};
const AuthContext = createContext<AuthContextType | undefined>(undefined);
const users: User[] = usersData;
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const login = (loginId: string, password: string) => {
const foundUser = users.find((user) => {
return (
(user.username === loginId || user.email === loginId) &&
password === "password"
);
});
if (!foundUser) {
return false;
}
setUser(foundUser);
return true;
};
const logout = () => {
setUser(null);
};
const isLoggedIn = user !== null;
return (
<AuthContext.Provider value={{ user, isLoggedIn, login, logout }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error("useAuth は AuthProvider の中で使ってください");
}
return context;
}
ここが一番大事です。
const [user, setUser] = useState<User | null>(null);
これは、
null = 未ログイン
User = ログイン中
という意味です。
⸻
- components/ProtectedRoute.tsx
import { Navigate } from "react-router-dom";
import { useAuth } from "../context/AuthContext";
type ProtectedRouteProps = {
children: React.ReactNode;
};
function ProtectedRoute({ children }: ProtectedRouteProps) {
const { isLoggedIn } = useAuth();
if (!isLoggedIn) {
return <Navigate to="/login" replace />;
}
return children;
}
export default ProtectedRoute;
このコンポーネントで囲まれたページは、未ログインなら /login に飛ばされます。
⸻
- components/Header.tsx
import { Link } from "react-router-dom";
import { useAuth } from "../context/AuthContext";
function Header() {
const { user, isLoggedIn, logout } = useAuth();
return (
<header>
<nav>
<Link to="/">ホーム</Link> |{" "}
<Link to="/about">About</Link> |{" "}
<Link to="/mypage">マイページ</Link> |{" "}
<Link to="/dashboard">ダッシュボード</Link> |{" "}
{!isLoggedIn && <Link to="/login">ログイン</Link>}
</nav>
<div>
{isLoggedIn ? (
<>
<p>{user?.name} さんでログイン中</p>
<button onClick={logout}>ログアウト</button>
</>
) : (
<p>未ログインです</p>
)}
</div>
</header>
);
}
export default Header;
⸻
- pages/HomePage.tsx
function HomePage() {
return (
<div>
<h1>ホーム</h1>
<p>このページはログインしていなくても見られます。</p>
</div>
);
}
export default HomePage;
⸻
- pages/AboutPage.tsx
function AboutPage() {
return (
<div>
<h1>About</h1>
<p>このページもログインなしで見られます。</p>
</div>
);
}
export default AboutPage;
⸻
- pages/LoginPage.tsx
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { useAuth } from "../context/AuthContext";
function LoginPage() {
const [loginId, setLoginId] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const { login, isLoggedIn } = useAuth();
const navigate = useNavigate();
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const result = login(loginId, password);
if (!result) {
setError("ユーザー名、メールアドレス、またはパスワードが違います");
return;
}
setError("");
navigate("/mypage");
};
if (isLoggedIn) {
return (
<div>
<h1>ログイン済みです</h1>
<button onClick={() => navigate("/mypage")}>マイページへ</button>
</div>
);
}
return (
<div>
<h1>ログイン</h1>
<form onSubmit={handleSubmit}>
<div>
<label>ユーザー名 または メールアドレス</label>
<input
type="text"
value={loginId}
onChange={(e) => setLoginId(e.target.value)}
/>
</div>
<div>
<label>パスワード</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<button type="submit">ログイン</button>
</form>
{error && <p>{error}</p>}
<hr />
<p>テスト用</p>
<p>ユーザー名:ando</p>
<p>メール:ando@test.com</p>
<p>パスワード:password</p>
</div>
);
}
export default LoginPage;
⸻
- pages/MyPage.tsx
import { useAuth } from "../context/AuthContext";
function MyPage() {
const { user } = useAuth();
return (
<div>
<h1>マイページ</h1>
<p>このページはログインしている人だけ見られます。</p>
<p>こんにちは、{user?.name} さん</p>
<p>メールアドレス:{user?.email}</p>
</div>
);
}
export default MyPage;
⸻
- pages/DashboardPage.tsx
function DashboardPage() {
return (
<div>
<h1>ダッシュボード</h1>
<p>ここもログインしている人だけ見られます。</p>
</div>
);
}
export default DashboardPage;
⸻
- App.tsx
import { Routes, Route } from "react-router-dom";
import Header from "./components/Header";
import ProtectedRoute from "./components/ProtectedRoute";
import HomePage from "./pages/HomePage";
import LoginPage from "./pages/LoginPage";
import AboutPage from "./pages/AboutPage";
import MyPage from "./pages/MyPage";
import DashboardPage from "./pages/DashboardPage";
function App() {
return (
<>
<Header />
<main>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/about" element={<AboutPage />} />
<Route path="/login" element={<LoginPage />} />
<Route
path="/mypage"
element={
<ProtectedRoute>
<MyPage />
</ProtectedRoute>
}
/>
<Route
path="/dashboard"
element={
<ProtectedRoute>
<DashboardPage />
</ProtectedRoute>
}
/>
</Routes>
</main>
</>
);
}
export default App;
⸻
- main.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import { AuthProvider } from "./context/AuthContext";
import App from "./App";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<BrowserRouter>
<AuthProvider>
<App />
</AuthProvider>
</BrowserRouter>
</React.StrictMode>
);
⸻
インストール
React Router がまだなら入れます。
npm install react-router-dom
⸻
仕組み
main.tsx
↓
AuthProvider でアプリ全体を囲む
↓
どのページからでも useAuth() が使える
↓
ログイン成功したら user にユーザー情報を入れる
↓
user が null じゃなければログイン中
React の Context は、props を何段も渡さずに深いコンポーネントへ値を渡すための仕組みです。React公式でも、createContext はコンポーネント外で作り、useContext で読み取る形が説明されています。 
⸻
記事に書くなら結論はこれ
const [user, setUser] = useState(null);
これがログイン状態の本体です。
user === null
→ 未ログイン
user にデータがある
→ ログイン中
AuthContext は保存場所ではなく、
user / login / logout を全ページで使えるようにする共有係です。
はい。
あなたの記事の続きなら、APIなし・モックJSONだけで、こう作るのが一番わかりやすいです。
参照先は「認証機能と保護されたルート」の章で、あなたの記事は今 App がログイン状態を持つところまでなので、次は AuthContext + React Router + ProtectedRoute に進める形が自然です。 
⸻
作るもの
未ログインでも見られるページ
- /
- /login
- /about
ログインしていないと見られないページ
- /mypage
- /dashboard
ログイン状態はここで管理します。
src/context/AuthContext.tsx
⸻
完成構成
src/
├ data/
│ └ users.json
├ types/
│ └ user.ts
├ context/
│ └ AuthContext.tsx
├ components/
│ ├ Header.tsx
│ └ ProtectedRoute.tsx
├ pages/
│ ├ HomePage.tsx
│ ├ LoginPage.tsx
│ ├ AboutPage.tsx
│ ├ MyPage.tsx
│ └ DashboardPage.tsx
├ App.tsx
└ main.tsx
⸻
- users.json
[
{
"id": 1,
"username": "ando",
"email": "ando@test.com",
"name": "安藤"
},
{
"id": 2,
"username": "tanaka",
"email": "tanaka@test.com",
"name": "田中"
}
]
パスワードは全員共通で password にします。
⸻
- types/user.ts
export type User = {
id: number;
username: string;
email: string;
name: string;
};
⸻
- context/AuthContext.tsx
import { createContext, useContext, useState } from "react";
import usersData from "../data/users.json";
import type { User } from "../types/user";
type AuthContextType = {
user: User | null;
isLoggedIn: boolean;
login: (loginId: string, password: string) => boolean;
logout: () => void;
};
const AuthContext = createContext<AuthContextType | undefined>(undefined);
const users: User[] = usersData;
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const login = (loginId: string, password: string) => {
const foundUser = users.find((user) => {
return (
(user.username === loginId || user.email === loginId) &&
password === "password"
);
});
if (!foundUser) {
return false;
}
setUser(foundUser);
return true;
};
const logout = () => {
setUser(null);
};
const isLoggedIn = user !== null;
return (
<AuthContext.Provider value={{ user, isLoggedIn, login, logout }}>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error("useAuth は AuthProvider の中で使ってください");
}
return context;
}
ここが一番大事です。
const [user, setUser] = useState<User | null>(null);
これは、
null = 未ログイン
User = ログイン中
という意味です。
⸻
- components/ProtectedRoute.tsx
import { Navigate } from "react-router-dom";
import { useAuth } from "../context/AuthContext";
type ProtectedRouteProps = {
children: React.ReactNode;
};
function ProtectedRoute({ children }: ProtectedRouteProps) {
const { isLoggedIn } = useAuth();
if (!isLoggedIn) {
return <Navigate to="/login" replace />;
}
return children;
}
export default ProtectedRoute
このコンポーネントで囲まれたページは、未ログインなら /login に飛ばされます。
⸻
- components/Header.tsx
import { Link } from "react-router-dom";
import { useAuth } from "../context/AuthContext";
function Header() {
const { user, isLoggedIn, logout } = useAuth();
return (
<header>
<nav>
<Link to="/">ホーム</Link> |{" "}
<Link to="/about">About</Link> |{" "}
<Link to="/mypage">マイページ</Link> |{" "}
<Link to="/dashboard">ダッシュボード</Link> |{" "}
{!isLoggedIn && <Link to="/login">ログイン</Link>}
</nav>
<div>
{isLoggedIn ? (
<>
<p>{user?.name} さんでログイン中</p>
<button onClick={logout}>ログアウト</button>
</>
) : (
<p>未ログインです</p>
)}
</div>
</header>
);
}
export default Header;
⸻
- pages/HomePage.tsx
function HomePage() {
return (
<div>
<h1>ホーム</h1>
<p>このページはログインしていなくても見られます。</p>
</div>
);
}
export default HomePage;
⸻
- pages/AboutPage.tsx
function AboutPage() {
return (
<div>
<h1>About</h1>
<p>このページもログインなしで見られます。</p>
</div>
);
}
export default AboutPage;
⸻
- pages/LoginPage.tsx
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { useAuth } from "../context/AuthContext";
function LoginPage() {
const [loginId, setLoginId] = useState("");
const [password, setPassword] = useState("");
const [error, setError] = useState("");
const { login, isLoggedIn } = useAuth();
const navigate = useNavigate();
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const result = login(loginId, password);
if (!result) {
setError("ユーザー名、メールアドレス、またはパスワードが違います");
return;
}
setError("");
navigate("/mypage");
};
if (isLoggedIn) {
return (
<div>
<h1>ログイン済みです</h1>
<button onClick={() => navigate("/mypage")}>マイページへ</button>
</div>
);
}
return (
<div>
<h1>ログイン</h1>
<form onSubmit={handleSubmit}>
<div>
<label>ユーザー名 または メールアドレス</label>
<input
type="text"
value={loginId}
onChange={(e) => setLoginId(e.target.value)}
/>
</div>
<div>
<label>パスワード</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
</div>
<button type="submit">ログイン</button>
</form>
{error && <p>{error}</p>}
<hr />
<p>テスト用</p>
<p>ユーザー名:ando</p>
<p>メール:ando@test.com</p>
<p>パスワード:password</p>
</div>
);
}
export default LoginPage;
⸻
- pages/MyPage.tsx
import { useAuth } from "../context/AuthContext";
function MyPage() {
const { user } = useAuth();
return (
<div>
<h1>マイページ</h1>
<p>このページはログインしている人だけ見られます。</p>
<p>こんにちは、{user?.name} さん</p>
<p>メールアドレス:{user?.email}</p>
</div>
);
}
export default MyPage;
⸻
- pages/DashboardPage.tsx
function DashboardPage() {
return (
<div>
<h1>ダッシュボード</h1>
<p>ここもログインしている人だけ見られます。</p>
</div>
);
}
export default DashboardPage;
⸻
- App.tsx
import { Routes, Route } from "react-router-dom";
import Header from "./components/Header";
import ProtectedRoute from "./components/ProtectedRoute";
import HomePage from "./pages/HomePage";
import LoginPage from "./pages/LoginPage";
import AboutPage from "./pages/AboutPage";
import MyPage from "./pages/MyPage";
import DashboardPage from "./pages/DashboardPage";
function App() {
return (
<>
<Header />
<main>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/about" element={<AboutPage />} />
<Route path="/login" element={<LoginPage />} />
<Route
path="/mypage"
element={
<ProtectedRoute>
<MyPage />
</ProtectedRoute>
}
/>
<Route
path="/dashboard"
element={
<ProtectedRoute>
<DashboardPage />
</ProtectedRoute>
}
/>
</Routes>
</main>
</>
);
}
export default App;
⸻
- main.tsx
import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import { AuthProvider } from "./context/AuthContext";
import App from "./App";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<BrowserRouter>
<AuthProvider>
<App />
</AuthProvider>
</BrowserRouter>
</React.StrictMode>
);
⸻
インストール
React Router がまだなら入れます。
npm install react-router-dom
⸻
仕組み
main.tsx
↓
AuthProvider でアプリ全体を囲む
↓
どのページからでも useAuth() が使える
↓
ログイン成功したら user にユーザー情報を入れる
↓
user が null じゃなければログイン中
React の Context は、props を何段も渡さずに深いコンポーネントへ値を渡すための仕組みです。React公式でも、createContext はコンポーネント外で作り、useContext で読み取る形が説明されています。 
⸻
const [user, setUser] = useState(null);
これがログイン状態の本体です。
user === null
→ 未ログイン
user にデータがある
→ ログイン中
AuthContext は保存場所ではなく、
user / login / logout を全ページで使えるようにする共有係です。
OKです。
「email + password でログインする版」 を、そのまま使える形で作ります。
今回は TypeScript + React + useContext です。
⸻
まず型を作る
src/types/auth.ts
export type LoginCredentials = {
email: string;
password: string;
};
export type User = {
id: number;
name: string;
email: string;
};
⸻
AuthContext
src/context/AuthContext.tsx
import { createContext, useContext, useState } from "react";
import type { LoginCredentials, User } from "../types/auth";
type AuthContextType = {
user: User | null;
isLoggedIn: boolean;
login: (credentials: LoginCredentials) => boolean;
logout: () => void;
};
const AuthContext = createContext<AuthContextType | undefined>(undefined);
// モックユーザー
const mockUser = {
id: 1,
name: "安藤",
email: "ando@test.com",
password: "password",
};
export function AuthProvider({
children,
}: {
children: React.ReactNode;
}) {
const [user, setUser] = useState<User | null>(null);
const login = (credentials: LoginCredentials): boolean => {
if (
credentials.email === mockUser.email &&
credentials.password === mockUser.password
) {
setUser({
id: mockUser.id,
name: mockUser.name,
email: mockUser.email,
});
return true;
}
return false;
};
const logout = () => {
setUser(null);
};
const isLoggedIn = user !== null;
return (
<AuthContext.Provider
value={{
user,
isLoggedIn,
login,
logout,
}}
>
{children}
</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error("useAuthはAuthProviderの中で使ってください");
}
return context;
}
⸻
LoginPage
src/pages/LoginPage.tsx
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { useAuth } from "../context/AuthContext";
import type { LoginCredentials } from "../types/auth";
function LoginPage() {
const { login } = useAuth();
const navigate = useNavigate();
const [form, setForm] = useState<LoginCredentials>({
email: "",
password: "",
});
const [error, setError] = useState("");
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setForm({
...form,
[e.target.name]: e.target.value,
});
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const result = login(form);
if (!result) {
setError("メールアドレスまたはパスワードが違います");
return;
}
setError("");
navigate("/mypage");
};
return (
<div>
<h1>ログイン</h1>
<form onSubmit={handleSubmit}>
<div>
<label>メールアドレス</label>
<input
type="email"
name="email"
value={form.email}
onChange={handleChange}
/>
</div>
<div>
<label>パスワード</label>
<input
type="password"
name="password"
value={form.password}
onChange={handleChange}
/>
</div>
<button type="submit">ログイン</button>
</form>
{error && <p>{error}</p>}
<hr />
<p>テスト用</p>
<p>メール: ando@test.com</p>
<p>パスワード: password</p>
</div>
);
}
export default LoginPage;
⸻
何が変わった?
前は
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
2個でした。
⸻
今回は
const [form, setForm] = useState({
email: "",
password: "",
});
1つにまとめています。
つまり
form = {
email: "",
password: "",
}
⸻
変更時
setForm({
...form,
[e.target.name]: e.target.value,
});
これで
email を変えたら
{
email: "aaa@test.com",
password: ""
}
password を変えたら
{
email: "aaa@test.com",
password: "1234"
}
になります。
⸻
AuthContext 側は
login(form)
で受け取ります。
中身:
{
email: "ando@test.com",
password: "password"
}
⸻
型の役割
ログイン入力
LoginCredentials
↓
ログイン成功
User
この分離がすごく大事です。
⸻
実務ではこの形がかなり近いです。
次は localStorage に保存して、リロードしてもログイン維持 に進むと本物っぽくなります。