Agent skill
web-navigation
Navigation and routing patterns for React web applications. Use when implementing React Router, Next.js routing, deep links, or handling navigation state.
Install this agent skill to your Project
npx add-skill https://github.com/aiskillstore/marketplace/tree/main/skills/cjharmath/web-navigation
SKILL.md
Web Navigation (React)
React Router (v6)
Basic Setup
// App.tsx
import { BrowserRouter, Routes, Route } from 'react-router-dom';
function App() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/about" element={<AboutPage />} />
<Route path="/users" element={<UsersPage />} />
<Route path="/users/:id" element={<UserDetailPage />} />
<Route path="*" element={<NotFoundPage />} />
</Routes>
</BrowserRouter>
);
}
Nested Routes & Layouts
// Layout with shared UI
function DashboardLayout() {
return (
<div className="dashboard">
<Sidebar />
<main>
<Outlet /> {/* Child routes render here */}
</main>
</div>
);
}
// Routes
<Routes>
<Route path="/dashboard" element={<DashboardLayout />}>
<Route index element={<DashboardHome />} />
<Route path="analytics" element={<Analytics />} />
<Route path="settings" element={<Settings />} />
</Route>
</Routes>
Dynamic Routes
import { useParams, useSearchParams } from 'react-router-dom';
// Route: /users/:id
function UserDetailPage() {
const { id } = useParams<{ id: string }>();
const [searchParams, setSearchParams] = useSearchParams();
const tab = searchParams.get('tab') || 'profile';
return (
<div>
<h1>User {id}</h1>
<TabBar
active={tab}
onChange={(t) => setSearchParams({ tab: t })}
/>
</div>
);
}
Programmatic Navigation
import { useNavigate, useLocation } from 'react-router-dom';
function LoginPage() {
const navigate = useNavigate();
const location = useLocation();
async function handleLogin() {
await login(credentials);
// Redirect to intended page or default
const from = location.state?.from?.pathname || '/dashboard';
navigate(from, { replace: true });
}
// Other navigation methods
navigate('/users'); // Push to history
navigate('/users', { replace: true }); // Replace current entry
navigate(-1); // Go back
navigate(1); // Go forward
}
Link Component
import { Link, NavLink } from 'react-router-dom';
// Basic link
<Link to="/about">About</Link>
// With state
<Link to="/checkout" state={{ cartId: '123' }}>
Checkout
</Link>
// NavLink - active styling
<NavLink
to="/dashboard"
className={({ isActive }) =>
isActive ? 'nav-link active' : 'nav-link'
}
>
Dashboard
</NavLink>
Next.js App Router
File-Based Routing
app/
├── layout.tsx # Root layout
├── page.tsx # / route
├── about/
│ └── page.tsx # /about route
├── users/
│ ├── page.tsx # /users route
│ └── [id]/
│ └── page.tsx # /users/:id route
├── (auth)/ # Route group (no URL segment)
│ ├── login/
│ │ └── page.tsx # /login route
│ └── register/
│ └── page.tsx # /register route
└── dashboard/
├── layout.tsx # Dashboard layout
├── page.tsx # /dashboard
└── settings/
└── page.tsx # /dashboard/settings
Layouts
// app/layout.tsx - Root layout
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<Providers>
<Header />
{children}
<Footer />
</Providers>
</body>
</html>
);
}
// app/dashboard/layout.tsx - Nested layout
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="dashboard">
<Sidebar />
<main>{children}</main>
</div>
);
}
Dynamic Routes
// app/users/[id]/page.tsx
interface Props {
params: { id: string };
searchParams: { tab?: string };
}
export default function UserPage({ params, searchParams }: Props) {
const { id } = params;
const tab = searchParams.tab || 'profile';
return (
<div>
<h1>User {id}</h1>
<Tabs active={tab} />
</div>
);
}
// Generate static params (optional)
export async function generateStaticParams() {
const users = await getUsers();
return users.map((user) => ({
id: user.id,
}));
}
Programmatic Navigation
'use client';
import { useRouter, usePathname, useSearchParams } from 'next/navigation';
function SearchForm() {
const router = useRouter();
const pathname = usePathname();
const searchParams = useSearchParams();
function handleSearch(query: string) {
const params = new URLSearchParams(searchParams);
params.set('q', query);
router.push(`${pathname}?${params.toString()}`);
}
// Navigation methods
router.push('/dashboard'); // Navigate
router.replace('/dashboard'); // Replace without history
router.back(); // Go back
router.forward(); // Go forward
router.refresh(); // Refresh server components
}
Link Component
import Link from 'next/link';
// Basic link
<Link href="/about">About</Link>
// With dynamic route
<Link href={`/users/${user.id}`}>
{user.name}
</Link>
// With query params
<Link href={{ pathname: '/search', query: { q: 'react' } }}>
Search
</Link>
// Prefetching (default: true)
<Link href="/dashboard" prefetch={false}>
Dashboard
</Link>
Route Groups & Organization
Auth-Protected vs Public Routes
// React Router
<Routes>
{/* Public routes */}
<Route path="/login" element={<LoginPage />} />
<Route path="/register" element={<RegisterPage />} />
{/* Protected routes */}
<Route element={<RequireAuth />}>
<Route path="/dashboard" element={<DashboardPage />} />
<Route path="/settings" element={<SettingsPage />} />
</Route>
</Routes>
// Next.js - use route groups
// app/(public)/login/page.tsx
// app/(protected)/dashboard/page.tsx
// app/(protected)/layout.tsx - add auth check
Loading & Error States
React Router
import { Suspense } from 'react';
import { Await, useLoaderData, defer } from 'react-router-dom';
// Loader
export async function loader({ params }) {
return defer({
user: getUser(params.id), // Promise
});
}
// Component
function UserPage() {
const { user } = useLoaderData();
return (
<Suspense fallback={<Spinner />}>
<Await resolve={user} errorElement={<ErrorFallback />}>
{(resolvedUser) => <UserProfile user={resolvedUser} />}
</Await>
</Suspense>
);
}
Next.js
// app/users/[id]/loading.tsx
export default function Loading() {
return <Spinner />;
}
// app/users/[id]/error.tsx
'use client';
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
}) {
return (
<div>
<h2>Something went wrong!</h2>
<button onClick={reset}>Try again</button>
</div>
);
}
// app/users/[id]/not-found.tsx
export default function NotFound() {
return <div>User not found</div>;
}
Scroll Restoration
React Router
import { ScrollRestoration } from 'react-router-dom';
function App() {
return (
<BrowserRouter>
<Routes>{/* ... */}</Routes>
<ScrollRestoration />
</BrowserRouter>
);
}
Manual Scroll to Top
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
function ScrollToTop() {
const { pathname } = useLocation();
useEffect(() => {
window.scrollTo(0, 0);
}, [pathname]);
return null;
}
Deep Linking / Query Params
// Custom hook for type-safe query params
import { useSearchParams } from 'react-router-dom';
interface Filters {
category?: string;
sort?: 'asc' | 'desc';
page?: number;
}
function useFilters() {
const [searchParams, setSearchParams] = useSearchParams();
const filters: Filters = {
category: searchParams.get('category') || undefined,
sort: (searchParams.get('sort') as 'asc' | 'desc') || undefined,
page: Number(searchParams.get('page')) || 1,
};
function setFilters(newFilters: Partial<Filters>) {
const params = new URLSearchParams(searchParams);
Object.entries(newFilters).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
params.set(key, String(value));
} else {
params.delete(key);
}
});
setSearchParams(params);
}
return { filters, setFilters };
}
// Usage
function ProductList() {
const { filters, setFilters } = useFilters();
return (
<div>
<CategorySelect
value={filters.category}
onChange={(cat) => setFilters({ category: cat })}
/>
<ProductGrid products={products} />
<Pagination
page={filters.page}
onChange={(p) => setFilters({ page: p })}
/>
</div>
);
}
Navigation Guards
// Prevent navigation with unsaved changes
import { useBlocker } from 'react-router-dom';
function EditForm() {
const [isDirty, setIsDirty] = useState(false);
const blocker = useBlocker(
({ currentLocation, nextLocation }) =>
isDirty && currentLocation.pathname !== nextLocation.pathname
);
return (
<>
<form onChange={() => setIsDirty(true)}>
{/* form fields */}
</form>
{blocker.state === 'blocked' && (
<ConfirmDialog
message="You have unsaved changes. Leave anyway?"
onConfirm={() => blocker.proceed()}
onCancel={() => blocker.reset()}
/>
)}
</>
);
}
Common Patterns
Redirect After Action
// After form submission
async function handleSubmit(data: FormData) {
const result = await createItem(data);
navigate(`/items/${result.id}`);
}
// After login
async function handleLogin() {
await login(credentials);
const redirectTo = searchParams.get('redirect') || '/dashboard';
navigate(redirectTo, { replace: true });
}
Tab Navigation with URL
function UserProfile() {
const [searchParams, setSearchParams] = useSearchParams();
const tab = searchParams.get('tab') || 'overview';
const tabs = ['overview', 'activity', 'settings'];
return (
<div>
<nav>
{tabs.map((t) => (
<button
key={t}
onClick={() => setSearchParams({ tab: t })}
className={tab === t ? 'active' : ''}
>
{t}
</button>
))}
</nav>
<TabContent tab={tab} />
</div>
);
}
Common Issues
| Issue | Solution |
|---|---|
| Route not matching | Check route order (specific before dynamic) |
| Back button doesn't work | Use navigate() not window.location |
| State lost on refresh | Store in URL params, not just state |
| Scroll position wrong | Add ScrollRestoration component |
| 404 in production (SPA) | Configure server for SPA fallback |
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
perigon-backend
Perigon ASP.NET Core + EF Core + Aspire conventions
perigon-agent
Pointers for Copilot/agents to apply Perigon conventions
perigon-angular
Angular 21+ standalone/Material/signal conventions for Perigon WebApp
fastapi-mastery
Comprehensive FastAPI development skill covering REST API creation, routing, request/response handling, validation, authentication, database integration, middleware, and deployment. Use when working with FastAPI projects, building APIs, implementing CRUD operations, setting up authentication/authorization, integrating databases (SQL/NoSQL), adding middleware, handling WebSockets, or deploying FastAPI applications. Triggered by requests involving .py files with FastAPI code, API endpoint creation, Pydantic models, or FastAPI-specific features.
context7-efficient
Token-efficient library documentation fetcher using Context7 MCP with 86.8% token savings through intelligent shell pipeline filtering. Fetches code examples, API references, and best practices for JavaScript, Python, Go, Rust, and other libraries. Use when users ask about library documentation, need code examples, want API usage patterns, are learning a new framework, need syntax reference, or troubleshooting with library-specific information. Triggers include questions like "Show me React hooks", "How do I use Prisma", "What's the Next.js routing syntax", or any request for library/framework documentation.
browser-use
Browser automation using Playwright MCP. Navigate websites, fill forms, click elements, take screenshots, and extract data. Use when tasks require web browsing, form submission, web scraping, UI testing, or any browser interaction.
Didn't find tool you were looking for?