Goal
Install TanStack Router, create real URLs (/login, /jobs, /saved), and guard private pages with a pathless _authenticated layout that redirects when there is no token cookie / Redux token.
You already learned TanStack Router basics in the React module. Here you wire it to RTK auth.
1. Install packages
npm install @tanstack/react-router@1.65.0
npm install -D @tanstack/router-plugin@1.65.0 @tanstack/router-devtools@1.65.0
2. Update vite.config.js
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import tailwindcss from "@tailwindcss/vite";
import { TanStackRouterVite } from "@tanstack/router-plugin/vite";
export default defineConfig({
plugins: [TanStackRouterVite(), tailwindcss(), react()],
});
Put the router plugin before React so file-based routes generate correctly.
Restart npm run dev after this change. TanStack will create src/routeTree.gen.ts when route files exist. Add that file to .gitignore if you like (it regenerates).
3. Root layout , src/routes/__root.jsx
import { useEffect } from "react";
import { useSelector } from "react-redux";
import { createRootRoute, Outlet } from "@tanstack/react-router";
import { TanStackRouterDevtools } from "@tanstack/router-devtools";
import Navbar from "../components/Navbar";
export const Route = createRootRoute({
component: RootLayout,
});
function RootLayout() {
const theme = useSelector((state) => state.ui.theme);
useEffect(() => {
document.documentElement.classList.toggle("dark", theme === "dark");
}, [theme]);
return (
<div className="min-h-screen bg-surface font-sans text-ink antialiased dark:bg-slate-950 dark:text-slate-100">
<Navbar />
<main className="mx-auto max-w-5xl px-6 py-10">
<Outlet />
</main>
<TanStackRouterDevtools position="bottom-right" />
</div>
);
}
Chunks explained
| Chunk | What it does |
|---|---|
createRootRoute |
Shell for every URL |
<Outlet /> |
Where child routes render |
Theme useEffect |
Keeps dark mode in sync on all pages |
4. Login route , src/routes/login.jsx
import { createFileRoute, redirect } from "@tanstack/react-router";
import { store } from "../store";
import { getCookie } from "../utils/cookies";
import LoginForm from "../components/LoginForm";
export const Route = createFileRoute("/login")({
beforeLoad: () => {
const token = store.getState().auth.token || getCookie("hireboard-token");
if (token) {
throw redirect({ to: "/jobs" });
}
},
component: LoginPage,
});
function LoginPage() {
return <LoginForm />;
}
Chunks explained
| Chunk | What it does |
|---|---|
beforeLoad |
Runs before the route renders |
store.getState() |
Read auth outside React |
redirect({ to: "/jobs" }) |
Already logged in → skip login page |
5. Protected layout , src/routes/_authenticated.jsx
The _ prefix makes a pathless layout: it does not add a URL segment, but wraps child routes.
import { createFileRoute, Outlet, redirect } from "@tanstack/react-router";
import { store } from "../store";
import { getCookie } from "../utils/cookies";
export const Route = createFileRoute("/_authenticated")({
beforeLoad: ({ location }) => {
const token = store.getState().auth.token || getCookie("hireboard-token");
if (!token) {
throw redirect({
to: "/login",
search: {
redirect: location.href,
},
});
}
},
component: AuthenticatedLayout,
});
function AuthenticatedLayout() {
return <Outlet />;
}
Chunks explained
| Chunk | What it does |
|---|---|
/_authenticated |
Pathless parent for private pages |
No token → /login |
Server-style guard (not just hiding UI) |
search.redirect |
Optional return URL after login |
Child <Outlet /> |
Renders /jobs or /saved |
This is your protected route. Every private page sits under this layout.
6. Jobs and Saved routes
src/routes/_authenticated/jobs.jsx:
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/_authenticated/jobs")({
component: JobsPage,
});
function JobsPage() {
return (
<section>
<h2 className="mb-2 text-2xl font-semibold">Open roles</h2>
<p className="text-muted dark:text-slate-400">
Protected route works. Real job API comes next.
</p>
</section>
);
}
src/routes/_authenticated/saved.jsx:
import { createFileRoute } from "@tanstack/react-router";
export const Route = createFileRoute("/_authenticated/saved")({
component: SavedPage,
});
function SavedPage() {
return (
<section>
<h2 className="mb-2 text-2xl font-semibold">Saved applications</h2>
<p className="text-muted dark:text-slate-400">
Only visible when authenticated.
</p>
</section>
);
}
Public URLs become /jobs and /saved (the _authenticated segment is not in the path).
7. Index redirect , src/routes/index.jsx
import { createFileRoute, redirect } from "@tanstack/react-router";
import { store } from "../store";
import { getCookie } from "../utils/cookies";
export const Route = createFileRoute("/")({
beforeLoad: () => {
const token = store.getState().auth.token || getCookie("hireboard-token");
throw redirect({ to: token ? "/jobs" : "/login" });
},
});
8. Slim App.jsx + main.jsx
App.jsx:
import { RouterProvider, createRouter } from "@tanstack/react-router";
import { routeTree } from "./routeTree.gen";
const router = createRouter({ routeTree });
function App() {
return <RouterProvider router={router} />;
}
export default App;
main.jsx stays Provider → App:
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { Provider } from "react-redux";
import { store } from "./store";
import App from "./App.jsx";
import "./style.css";
createRoot(document.getElementById("root")).render(
<StrictMode>
<Provider store={store}>
<App />
</Provider>
</StrictMode>,
);
9. Update Navbar.jsx with router links
import { Link, useNavigate, useRouterState } from "@tanstack/react-router";
import { useDispatch, useSelector } from "react-redux";
import { logout } from "../store/authSlice";
import ThemeToggle from "./ThemeToggle";
function Navbar() {
const user = useSelector((state) => state.auth.user);
const dispatch = useDispatch();
const navigate = useNavigate();
const pathname = useRouterState({ select: (s) => s.location.pathname });
const linkBase = "rounded-md px-3 py-2 text-sm font-medium transition";
const active = "bg-brand-600 text-white";
const idle =
"text-slate-700 hover:bg-slate-100 dark:text-slate-200 dark:hover:bg-slate-800";
function handleLogout() {
dispatch(logout());
navigate({ to: "/login" });
}
return (
<header className="sticky top-0 z-10 border-b border-slate-200 bg-card/90 backdrop-blur dark:border-slate-800 dark:bg-slate-900/90">
<div className="mx-auto flex max-w-5xl items-center justify-between gap-4 px-6 py-3">
<div className="flex items-center gap-6">
<Link
to={user ? "/jobs" : "/login"}
className="text-lg font-semibold text-brand-600"
>
HireBoard
</Link>
{user && (
<nav className="flex gap-1">
<Link
to="/jobs"
className={`${linkBase} ${pathname === "/jobs" ? active : idle}`}
>
Jobs
</Link>
<Link
to="/saved"
className={`${linkBase} ${pathname === "/saved" ? active : idle}`}
>
Saved
</Link>
</nav>
)}
</div>
<div className="flex items-center gap-3">
{user && (
<>
<span className="hidden text-sm text-muted sm:inline dark:text-slate-400">
{user.firstName} {user.lastName}
</span>
<button
type="button"
onClick={handleLogout}
className="rounded-md border border-slate-300 px-3 py-2 text-sm font-medium text-slate-700 hover:bg-slate-100 dark:border-slate-600 dark:text-slate-200 dark:hover:bg-slate-800"
>
Log out
</button>
</>
)}
<ThemeToggle />
</div>
</div>
</header>
);
}
export default Navbar;
Chunks explained
| Chunk | What it does |
|---|---|
<Link to="/jobs"> |
Client-side navigation (no full reload) |
useRouterState |
Read current path for active styles |
navigate({ to: "/login" }) |
After logout, leave protected URLs |
10. Navigate after login in LoginForm
After setCredentials, send the user to Jobs:
import { useNavigate } from "@tanstack/react-router";
// inside component:
const navigate = useNavigate();
// after dispatch(setCredentials(data)):
navigate({ to: "/jobs" });
11. Session restore checklist
| Key | Storage | Purpose |
|---|---|---|
hireboard-token |
Cookie | Access token for guards + API headers |
hireboard-user |
localStorage |
Profile for the navbar |
hireboard-theme |
localStorage |
Light / dark |
On refresh of /jobs:
- Store hydrates token from cookie and user from
localStorage. _authenticatedbeforeLoadsees the token → allows the route.- Navbar shows the user’s name.
Security note
| Practice | Reality in this demo |
|---|---|
| Token cookie set from JS | Fine for learning; production prefers httpOnly cookies from the server |
beforeLoad redirect |
Protects UI routes , APIs must still validate tokens |
| DummyJSON JWT | Demo credentials only |
Check
- Visit
/jobswhile logged out → redirect to/login. - Sign in → land on
/jobs; URL bar shows/jobs. - Visit
/saved→ works only when authenticated. - Log out → cookie + user cleared; back on
/login. - Refresh on
/jobswhile logged in → still there.
Assessment: Taskly : Protected routes
Install TanStack Router. Create /login, /tasks, and /pinned. Guard /tasks and /pinned with a pathless _authenticated layout (beforeLoad + redirect when no token).
Done when: guests cannot open /tasks or /pinned; login lands on /tasks; logout returns to /login.
Next: fetch real jobs from JSONFakery with RTK Query.