ログイン後のSidebarをroleで出し分ける #React
はじめに
前回の記事で作った工場管理システム(React + TypeScript + AuthContext + React Router v6)に、role によってリンクを出し分けるサイドバーを追加します。
やることは2つだけです。
-
useAuth()でuser.roleを取得する - リンク一覧を
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を追加
- 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";
- 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;
}
- App.tsx に追加
useNavigate() を使うので BrowserRouter の内側に入れる必要があります。
import { RoleRedirectProvider } from "./context/RoleRedirectProvider";
export default function App() {
return (
<AuthProvider>
<BrowserRouter>
<RoleRedirectProvider> {/* ← BrowserRouterの内側に置く */}
<Routes>
...
</Routes>
</RoleRedirectProvider>
</BrowserRouter>
</AuthProvider>
);
}
- 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です。