frontend-routing
Implement client-side routing using React Router, Vue Router, and Angular Router. Use when building multi-page applications with navigation and route protection.
Installation
Copy to your project
cp -r skills/frontend-routing/ /your-project/.claude/skills/frontend-routing/
Frontend Routing
Overview
Implement client-side routing with navigation, lazy loading, protected routes, and state management for multi-page single-page applications.
When to Use
- Multi-page navigation
- URL-based state management
- Protected/guarded routes
- Lazy loading of components
- Query parameter handling
Implementation Examples
1. React Router v6
// App.tsx
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
import { Layout } from './components/Layout';
import { Home } from './pages/Home';
import { NotFound } from './pages/NotFound';
import { useAuth } from './hooks/useAuth';
import React from 'react';
// Lazy loaded components
const Dashboard = React.lazy(() => import('./pages/Dashboard'));
const UserProfile = React.lazy(() => import('./pages/UserProfile'));
const Settings = React.lazy(() => import('./pages/Settings'));
// Protected route wrapper
const ProtectedRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const { isAuthenticated } = useAuth();
if (!isAuthenticated) {
return <Navigate to="/login" replace />;
}
return <>{children}</>;
};
export const App: React.FC = () => {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Home />} />
<Route
path="dashboard"
element={
<ProtectedRoute>
<React.Suspense fallback={<div>Loading...</div>}>
<Dashboard />
</React.Suspense>
</ProtectedRoute>
}
/>
<Route
path="users/:id"
element={
<React.Suspense fallback={<div>Loading...</div>}>
<UserProfile />
</React.Suspense>
}
/>
<Route path="settings" element={<Settings />} />
<Route path="*" element={<NotFound />} />
</Route>
</Routes>
</BrowserRouter>
);
};
// Usage in components
import { useParams, useNavigate, useSearchParams } from 'react-router-dom';
const UserProfile: React.FC = () => {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const tab = searchParams.get('tab') || 'profile';
return (
<div>
<h1>User {id}</h1>
<p>Tab: {tab}</p>
<button onClick={() => navigate('/')}>Go Home</button>
<button onClick={() => navigate('?tab=settings')}>Settings</button>
</div>
);
};
2. Vue Router 4
// router/index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from "vue-router";
import { useAuthStore } from "@/stores/auth";
const routes: RouteRecordRaw[] = [
{
path: "/",
component: () => import("@/views/Home.vue"),
meta: { title: "Home" },
},
{
path: "/login",
component: () => import("@/views/Login.vue"),
meta: { title: "Login", requiresGuest: true },
},
{
path: "/dashboard",
component: () => import("@/views/Dashboard.vue"),
meta: { title: "Dashboard", requiresAuth: true },
children: [
{
path: "users",
component: () => import("@/views/Users.vue"),
meta: { title: "Users" },
},
{
path: "analytics",
component: () => import("@/views/Analytics.vue"),
meta: { title: "Analytics" },
},
],
},
{
path: "/users/:id",
component: () => import("@/views/UserDetail.vue"),
meta: { title: "User Details", requiresAuth: true },
},
{
path: "/:pathMatch(.*)*",
component: () => import("@/views/NotFound.vue"),
meta: { title: "Not Found" },
},
];
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes,
});
// Navigation guards
router.beforeEach((to, from, next) => {
const authStore = useAuthStore();
// Update page title
document.title = (to.meta.title as string) || "App";
// Check authentication
if (to.meta.requiresAuth && !authStore.isAuthenticated) {
next("/login");
} else if (to.meta.requiresGuest && authStore.isAuthenticated) {
next("/dashboard");
} else {
next();
}
});
export default router;
// main.ts
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
createApp(App).use(router).mount("#app");
3. Angular Routing
// app-routing.module.ts
import { NgModule } from "@angular/core";
import { RouterModule, Routes } from "@angular/router";
import { DashboardComponent } from "./components/dashboard/dashboard.component";
import { AuthGuard } from "./guards/auth.guard";
import { GuestGuard } from "./guards/guest.guard";
const routes: Routes = [
{ path: "", redirectTo: "/home", pathMatch: "full" },
{
path: "home",
loadComponent: () =>
import("./pages/home/home.component").then((m) => m.HomeComponent),
},
{
path: "login",
loadComponent: () =>
import("./pages/login/login.component").then((m) => m.LoginComponent),
canActivate: [GuestGuard],
},
{
path: "dashboard",
component: DashboardComponent,
canActivate: [AuthGuard],
children: [
{
path: "users",
loadChildren: () =>
import("./features/users/users.module").then((m) => m.UsersModule),
},
],
},
{
path: "users/:id",
loadComponent: () =>
import("./pages/user-detail/user-detail.component").then(
(m) => m.UserDetailComponent,
),
canActivate: [AuthGuard],
},
{ path: "**", redirectTo: "/home" },
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class AppRoutingModule {}
// auth.guard.ts
import { Injectable } from "@angular/core";
import {
CanActivate,
ActivatedRouteSnapshot,
RouterStateSnapshot,
Router,
} from "@angular/router";
import { AuthService } from "../services/auth.service";
@Injectable({ providedIn: "root" })
export class AuthGuard implements CanActivate {
constructor(
private authService: AuthService,
private router: Router,
) {}
canActivate(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
): boolean {
if (this.authService.isAuthenticated()) {
return true;
}
this.router.navigate(["/login"], { queryParams: { returnUrl: state.url } });
return false;
}
}
// Component usage
import { Component, OnInit } from "@angular/core";
import { ActivatedRoute, Router } from "@angular/router";
@Component({
selector: "app-user-detail",
templateUrl: "./user-detail.component.html",
})
export class UserDetailComponent implements OnInit {
userId: string | null = null;
tab: string = "profile";
constructor(
private route: ActivatedRoute,
private router: Router,
) {}
ngOnInit(): void {
this.route.params.subscribe((params) => {
this.userId = params["id"];
});
this.route.queryParams.subscribe((params) => {
this.tab = params["tab"] || "profile";
});
}
goHome(): void {
this.router.navigate(["/"]);
}
navigateToTab(tab: string): void {
this.router.navigate([], {
relativeTo: this.route,
queryParams: { tab },
queryParamsHandling: "merge",
});
}
}
4. Query Parameter Handling
// React Hook for Query Params
import { useSearchParams } from 'react-router-dom';
const SearchUsers: React.FC = () => {
const [searchParams, setSearchParams] = useSearchParams();
const handleSearch = (query: string) => {
setSearchParams({ q: query, page: '1' });
};
const query = searchParams.get('q') || '';
const page = searchParams.get('page') || '1';
return (
<div>
<input
value={query}
onChange={(e) => handleSearch(e.target.value)}
placeholder="Search..."
/>
<p>Results for: {query} (Page {page})</p>
</div>
);
};
// Vue Query Param Hook
import { useRoute, useRouter } from 'vue-router';
import { computed } from 'vue';
export function useQueryParams() {
const route = useRoute();
const router = useRouter();
const query = computed(() => route.query.q as string || '');
const page = computed(() => parseInt(route.query.page as string) || 1);
const setQuery = (q: string) => {
router.push({ query: { q, page: '1' } });
};
return { query, page, setQuery };
}
5. Route Transition Effects
/* CSS Transition */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.slide-enter-active,
.slide-leave-active {
transition: transform 0.3s ease;
}
.slide-enter-from {
transform: translateX(-100%);
}
.slide-leave-to {
transform: translateX(100%);
}
Best Practices
- Use lazy loading for code splitting
- Implement route guards for protection
- Handle 404 routes appropriately
- Preserve scroll position
- Use query parameters for filters
- Implement breadcrumb navigation
- Manage route transitions smoothly
- Use named routes for maintainability