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?

ログイン後のSidebarをroleで出し分ける

1
Last updated at Posted at 2026-05-23

ログイン後のSidebarをroleで出し分ける #React

はじめに

前回の記事で作った工場管理システム(React + TypeScript + AuthContext + React Router v6)に、role によってリンクを出し分けるサイドバーを追加します。

やることは2つだけです。

  1. useAuth()user.role を取得する
  2. リンク一覧を role でフィルタリングして NavLink を並べる

前提:types/user.ts の UserRole を更新する

// src/types/user.ts

export type UserRole =
  | "worker"   // 作業者
  | "planner"  // 計画者
  | "manager"; // 管理者

export type UserStatus = "active" | "inactive";

export type User = {
  id: number;
  username: string;
  email: string;
  name: string;
  role: UserRole;
  department: string;
  line: string;
  status: UserStatus;
};

Sidebar.tsx

// src/components/Sidebar.tsx

import { NavLink } from "react-router-dom";
import { useAuth } from "../context/AuthContext";
import type { UserRole } from "../types/user";


// ============================================================
// ナビゲーションリンクの定義
//
// allowedRoles に含まれているroleだけが、そのリンクを見られます。
// リンクを増やすときはここに追加するだけ。
// ============================================================
const NAV_LINKS: {
  to: string;
  label: string;
  allowedRoles: UserRole[];
}[] = [
  // 全員が見られる
  { to: "/dashboard", label: "ダッシュボード", allowedRoles: ["worker", "planner", "manager"] },
  { to: "/my-tasks",  label: "自分の作業",     allowedRoles: ["worker", "planner", "manager"] },
  { to: "/schedule",  label: "スケジュール",   allowedRoles: ["worker", "planner", "manager"] },

  // planner と manager だけ
  { to: "/production-plan", label: "生産計画", allowedRoles: ["planner", "manager"] },
  { to: "/process",         label: "工程管理", allowedRoles: ["planner", "manager"] },

  // manager だけ
  { to: "/reports",  label: "レポート",     allowedRoles: ["manager"] },
  { to: "/staff",    label: "人員管理",     allowedRoles: ["manager"] },
  { to: "/settings", label: "システム設定", allowedRoles: ["manager"] },
];


export default function Sidebar() {
  const { user } = useAuth();

  // user が null(未ログイン)なら何も表示しない
  if (!user) return null;

  // ✅ user.role が allowedRoles に含まれているリンクだけに絞り込む
  // Array.filter()   → 条件を満たす要素だけ残した新しい配列を返す
  // Array.includes() → 配列の中にその値があるか true/false で返す
  const visibleLinks = NAV_LINKS.filter((link) =>
    link.allowedRoles.includes(user.role)
  );

  return (
    <nav style={styles.sidebar} aria-label="サイドバーナビゲーション">
      <ul style={styles.list}>
        {visibleLinks.map((link) => (
          <li key={link.to}>
            <NavLink
              to={link.to}
              style={({ isActive }) => ({
                ...styles.link,
                // isActive = 現在のURLと一致しているか(NavLink が自動で判断)
                background: isActive ? "#334155" : "transparent",
                color:      isActive ? "#f1f5f9" : "#94a3b8",
                fontWeight: isActive ? 600 : 400,
              })}
            >
              {link.label}
            </NavLink>
          </li>
        ))}
      </ul>
    </nav>
  );
}


const styles: Record<string, React.CSSProperties> = {
  sidebar: {
    width: "200px",
    minHeight: "100vh",
    background: "#1e293b",
    borderRight: "1px solid #334155",
    flexShrink: 0,
  },
  list: {
    listStyle: "none",
    margin: 0,
    padding: "12px 8px",
    display: "flex",
    flexDirection: "column",
    gap: "2px",
  },
  link: {
    display: "block",
    padding: "9px 14px",
    borderRadius: "6px",
    textDecoration: "none",
    fontSize: "13px",
    transition: "all 0.15s",
  },
};

使い方:Dashboard.tsx に組み込む

サイドバーとメインコンテンツを横並びにします。

// src/pages/Dashboard.tsx(抜粋)

import NavBar from "../components/NavBar";
import Sidebar from "../components/Sidebar"; // ← 追加

export default function Dashboard() {
  return (
    <div style={{ minHeight: "100vh", background: "#0f172a" }}>
      <NavBar />
      <div style={{ display: "flex" }}>
        <Sidebar />
        <main style={{ flex: 1, padding: "32px" }}>
          {/* メインコンテンツ */}
        </main>
      </div>
    </div>
  );
}

全ページ共通のレイアウトにしたい場合は Layout.tsx にまとめて <Outlet /> と組み合わせると便利です。

// src/components/Layout.tsx

import { Outlet } from "react-router-dom";
import NavBar from "./NavBar";
import Sidebar from "./Sidebar";

export default function Layout() {
  return (
    <div style={{ minHeight: "100vh", background: "#0f172a" }}>
      <NavBar />
      <div style={{ display: "flex" }}>
        <Sidebar />
        <main style={{ flex: 1, padding: "32px" }}>
          <Outlet /> {/* ← 各ページのコンテンツがここに入る */}
        </main>
      </div>
    </div>
  );
}
// src/App.tsx(Layout を使う場合)

<Route element={<RequireAuth />}>
  <Route element={<Layout />}>
    <Route path="/dashboard"      element={<Dashboard />} />
    <Route path="/my-tasks"       element={<MyTasks />} />
    <Route path="/schedule"       element={<Schedule />} />
    <Route path="/production-plan" element={<ProductionPlan />} />
    <Route path="/process"        element={<Process />} />
    <Route path="/reports"        element={<Reports />} />
    <Route path="/staff"          element={<Staff />} />
    <Route path="/settings"       element={<Settings />} />
  </Route>
</Route>

ポイントまとめ

フィルタリングの核心はこの1行

const visibleLinks = NAV_LINKS.filter((link) =>
  link.allowedRoles.includes(user.role)
);

リンクを増やすときは NAV_LINKS に追加するだけ

{ to: "/maintenance", label: "設備管理", allowedRoles: ["planner", "manager"] },

フィルタリングのロジックは変える必要がありません。


⚠️ 注意

サイドバーのリンク非表示は見た目の制限です。URLを直接入力されるとそのページにアクセスできてしまいます。roleに応じたアクセス制限はサーバーサイドでも必ず実施してください。

ログイン後にロールに従って遷移した場合

現状: LoginForm.tsx の handleSubmit で成功時は常に /dashboard へ遷移
やりたいこと: ログインしたユーザーの role(admin / supervisor / operator)によって飛び先を変える
変更は LoginForm.tsx の1箇所だけです。

修正箇所:src/components/LoginForm.tsx
handleSubmit の中のここを変えます。
変更前:

if (result === "success") {
  navigate("/dashboard");
}

変更後:

if (result === "success") {
  // login() が成功した直後、AuthContext の user はまだ state 更新中なので
  // users.json を直接引いてロールを確認する
  const found = USERS.find(
    (u) => u.username === loginId || u.email === loginId
  );
  
  const roleRedirect: Record<string, string> = {
    admin:      "/dashboard",
    supervisor: "/production",
    operator:   "/my-tasks",   // ← 飛ばしたいパスに変更
  };

  const destination = found ? (roleRedirect[found.role] ?? "/dashboard") : "/dashboard";
  navigate(destination);
}

なぜ found を再取得するのか?
login() を呼んだ直後は setUser(found) の state 更新がまだ反映されていません(React の state は非同期で更新される)。なので useAuth().user をすぐ読んでも null のままです。
すでに loginId という変数があるので、同じ USERS 配列をもう一度 .find() するのが一番シンプルです。

もっとスッキリ書くなら:login() の戻り値を変える
AuthContext.tsx の login() の戻り値を LoginResult から { result, user } に変えることで、再検索なしで書けます。ただし AuthContext.tsx と LoginForm.tsx の両方を変える必要があるので、今は USERS.find() の方が変更が少なくて安全です。

role に応じたリダイレクト先を一元管理する仕組み

作るファイルと構成

src/
├── config/
│   └── roleRedirect.ts     ← ★新規:ロール別リダイレクト先の定義
├── context/
│   └── RoleRedirectProvider.tsx  ← ★新規:Providerとカスタムフック
├── components/
│   └── LoginForm.tsx       ← 修正:useRoleRedirect() を使うだけ
└── App.tsx                 ← 修正:RoleRedirectProviderを追加
  1. src/config/roleRedirect.ts
    リダイレクト先をここだけで管理します。
import type { UserRole } from "../types/user";

export const ROLE_REDIRECT: Record<UserRole, string> = {
  admin:      "/dashboard",
  supervisor: "/production",
  operator:   "/my-tasks",
};

export const DEFAULT_REDIRECT = "/dashboard";
  1. src/context/RoleRedirectProvider.tsx
import { createContext, useContext, ReactNode } from "react";
import { useNavigate } from "react-router-dom";
import type { UserRole } from "../types/user";
import { ROLE_REDIRECT, DEFAULT_REDIRECT } from "../config/roleRedirect";

type RoleRedirectContextType = {
  redirectByRole: (role: UserRole) => void;
};

const RoleRedirectContext = createContext<RoleRedirectContextType | null>(null);

export function RoleRedirectProvider({ children }: { children: ReactNode }) {
  const navigate = useNavigate();

  const redirectByRole = (role: UserRole) => {
    const destination = ROLE_REDIRECT[role] ?? DEFAULT_REDIRECT;
    navigate(destination);
  };

  return (
    <RoleRedirectContext.Provider value={{ redirectByRole }}>
      {children}
    </RoleRedirectContext.Provider>
  );
}

export function useRoleRedirect(): RoleRedirectContextType {
  const context = useContext(RoleRedirectContext);
  if (!context) {
    throw new Error("useRoleRedirect() は RoleRedirectProvider の内側で使ってください");
  }
  return context;
}
  1. App.tsx に追加
    useNavigate() を使うので BrowserRouter の内側に入れる必要があります。
import { RoleRedirectProvider } from "./context/RoleRedirectProvider";

export default function App() {
  return (
    <AuthProvider>
      <BrowserRouter>
        <RoleRedirectProvider>  {/* ← BrowserRouterの内側に置く */}
          <Routes>
            ...
          </Routes>
        </RoleRedirectProvider>
      </BrowserRouter>
    </AuthProvider>
  );
}
  1. LoginForm.tsx の修正
// 追加
import { useRoleRedirect } from "../context/RoleRedirectProvider";

export default function LoginForm() {
  const { redirectByRole } = useRoleRedirect(); // ← 追加
  // ...

  const handleSubmit = async () => {
    // ...
    const result = login(loginId, password);

    if (result === "success") {
      const found = USERS.find(
        (u) => u.username === loginId || u.email === loginId
      );
      if (found) redirectByRole(found.role); // ← navigate("/dashboard") から変更
    }
  };
}

ポイントまとめ

|ファイル                      |役割                            |
|--------------------------|------------------------------|
|`roleRedirect.ts`         |飛び先の定義だけ。ここを変えれば全体に反映         |
|`RoleRedirectProvider.tsx`|`navigate` を内包。呼び出し側はroleを渡すだけ|
|`LoginForm.tsx`           |`redirectByRole(role)` の1行で完結 |

飛び先を変えたいときは roleRedirect.ts だけ触ればOKです。​​​​​​​​​​​​​​​​

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?