1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ログイン画面の練習をする

1
Last updated at Posted at 2026-05-18

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

実務でも近い構成です。

  1. モック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": "鈴木"
  }
]

  1. コンポーネント設計

役割分担します。

コンポーネント 役割
App 全体管理
LoginForm ログインフォーム
InputField 入力欄
Button ボタン
Welcome 成功画面

  1. 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;

  1. Button

components/Button.jsx

function Button({ text, onClick }) {
  return (
    <button onClick={onClick}>
      {text}
    </button>
  );
}

export default Button;

  1. Welcome

components/Welcome.jsx

function Welcome({ user }) {
  return (
    <div>
      <h2>ようこそ {user.name} さん</h2>
    </div>
  );
}

export default Welcome;

  1. 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;

  1. 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;
};
  1. users.json

src/data/users.json

[
  {
    "id": 1,
    "username": "test",
    "email": "test@test.com",
    "name": "テスト"
  },
  {
    "id": 2,
    "username": "tanaka",
    "email": "tanaka@test.com",
    "name": "田中"
  }
]
  1. 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;
  1. Button.tsx
type ButtonProps = {
  text: string;
  onClick: () => void;
};

function Button({ text, onClick }: ButtonProps) {
  return <button onClick={onClick}>{text}</button>;
}

export default Button;
  1. Welcome.tsx
import type { User } from "../types/user";

type WelcomeProps = {
  user: User;
};

function Welcome({ user }: WelcomeProps) {
  return <h2>ようこそ {user.name} さん</h2>;
}

export default Welcome;
  1. 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;
  1. 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

  1. users.json
[
  {
    "id": 1,
    "username": "ando",
    "email": "ando@test.com",
    "name": "安藤"
  },
  {
    "id": 2,
    "username": "tanaka",
    "email": "tanaka@test.com",
    "name": "田中"
  }
]

パスワードは全員共通で password にします。

  1. types/user.ts
export type User = {
  id: number;
  username: string;
  email: string;
  name: string;
};

  1. 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 = ログイン中

という意味です。

  1. 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 に飛ばされます。

  1. 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;

  1. pages/HomePage.tsx
function HomePage() {
  return (
    <div>
      <h1>ホーム</h1>
      <p>このページはログインしていなくても見られます</p>
    </div>
  );
}

export default HomePage;

  1. pages/AboutPage.tsx
function AboutPage() {
  return (
    <div>
      <h1>About</h1>
      <p>このページもログインなしで見られます</p>
    </div>
  );
}

export default AboutPage;

  1. 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;

  1. 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;

  1. pages/DashboardPage.tsx
function DashboardPage() {
  return (
    <div>
      <h1>ダッシュボード</h1>
      <p>ここもログインしている人だけ見られます</p>
    </div>
  );
}

export default DashboardPage;

  1. 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;

  1. 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

  1. users.json
[
  {
    "id": 1,
    "username": "ando",
    "email": "ando@test.com",
    "name": "安藤"
  },
  {
    "id": 2,
    "username": "tanaka",
    "email": "tanaka@test.com",
    "name": "田中"
  }
]

パスワードは全員共通で password にします。

  1. types/user.ts
export type User = {
  id: number;
  username: string;
  email: string;
  name: string;
};

  1. 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 = ログイン中

という意味です。

  1. 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 に飛ばされます。

  1. 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;

  1. pages/HomePage.tsx
function HomePage() {
  return (
    <div>
      <h1>ホーム</h1>
      <p>このページはログインしていなくても見られます。</p>
    </div>
  );
}

export default HomePage;

  1. pages/AboutPage.tsx
function AboutPage() {
  return (
    <div>
      <h1>About</h1>
      <p>このページもログインなしで見られます。</p>
    </div>
  );
}

export default AboutPage;

  1. 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;

  1. 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;

  1. pages/DashboardPage.tsx
function DashboardPage() {
  return (
    <div>
      <h1>ダッシュボード</h1>
      <p>ここもログインしている人だけ見られます。</p>
    </div>
  );
}

export default DashboardPage;

  1. 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;

  1. 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 に保存して、リロードしてもログイン維持 に進むと本物っぽくなります。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?