SPA Routing Guidelines
This document outlines the process for adding new routes to our Single Page Application (SPA) routing system.
Overview
Our application uses a custom SPA routing system built on top of Next.js. The routing system is implemented in the src/client/router directory and consists of:
- •A
RouterProvidercomponent that manages navigation and renders the current route - •Route components organized in folders within the src/client/routes directory
- •Route configuration in the src/client/routes/index.ts file
- •Navigation components in the src/client/components/layout directory
- •Route persistence via
useUIStore(Zustand) - remembers the last visited route for PWA instant boot
Adding a New Route
Follow these steps to add a new route to the application:
1. Create a Route Component Folder
Create a new folder in the src/client/routes directory with the name of your route component:
src/client/routes/ ├── NewRoute/ │ ├── NewRoute.tsx │ └── index.ts
Component Organization Guidelines
Follow these best practices for route components:
- •
Keep route components focused and small: The main route component should be primarily responsible for layout and composition, not complex logic.
- •
Split large components: If a route component is getting too large (over 200-300 lines), split it into multiple smaller components within the same route folder.
- •
Route-specific components: Components that are only used by a specific route should be placed in that route's folder.
- •
Shared components: If a component is used by multiple routes, move it to
src/client/componentsdirectory. - •
Component hierarchy:
codesrc/client/routes/NewRoute/ # Route-specific folder ├── NewRoute.tsx # Main route component (exported) ├── hooks.ts # React Query hooks for this route ├── NewRouteHeader.tsx # Route-specific component ├── NewRouteContent.tsx # Route-specific component └── index.ts # Exports the main component src/client/components/ # Shared components ├── SharedComponent.tsx # Used by multiple routes └── ...
- •
Colocate React Query hooks in a
hooks.tsfile within the route folder (unless shared across routes) - •
Extract business logic into separate hooks or utility functions
- •
Follow the naming convention of PascalCase for component files and folders
- •
Use named exports (avoid default exports as per our guidelines)
- •
Keep related components and utilities in the same folder
Data Fetching Pattern
Routes that fetch data should use React Query hooks (NOT direct API calls):
// src/client/routes/Todos/hooks.ts
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { getTodos, createTodo } from '@/apis/todos/client';
export const todosQueryKey = ['todos'] as const;
export function useTodos() {
return useQuery({
queryKey: todosQueryKey,
queryFn: async () => {
const response = await getTodos({});
if (response.data?.error) throw new Error(response.data.error);
return response.data;
},
});
}
// src/client/routes/Todos/Todos.tsx
export const Todos = () => {
const { data, isLoading, error } = useTodos();
if (isLoading && !data) return <LoadingSpinner />;
if (error) return <ErrorDisplay error={error} />;
return <TodoList todos={data?.todos || []} />;
};
Key points:
- •Data loads instantly from localStorage cache (React Query persistence)
- •Background revalidation keeps data fresh
- •Use
isLoading && !datato show loading only on initial load (not cache) - •See
state-management-guidelines.mdcanddocs/react-query-mutations.mdfor React Query patterns
2. Register the Route in the Routes Configuration
Add your new route to the routes configuration in src/client/routes/index.ts:
// Import your new route component
import { NewRoute } from './NewRoute';
// Add it to the routes configuration
export const routes = createRoutes({
'/': Home,
'/ai-chat': AIChat,
'/settings': Settings,
'/file-manager': FileManager,
'/new-route': NewRoute, // Add your new route here
'/not-found': NotFound,
});
Route Metadata
Routes can be configured with metadata using the RouteConfig object format:
export const routes = createRoutes({
// Simple route (requires authentication)
'/dashboard': Dashboard,
// Public route (no authentication required)
'/share/:id': { component: SharePage, public: true },
// Admin-only route
'/admin/reports': { component: Reports, adminOnly: true },
});
| Property | Type | Default | Description |
|---|---|---|---|
component | React.ComponentType | required | The route component |
public | boolean | false | If true, bypasses authentication in AuthWrapper |
adminOnly | boolean | false | If true, requires admin access |
When to use public: true:
- •Share pages that should be accessible without login
- •Landing pages or marketing pages
- •Public API documentation pages
How it works:
- •
RouterProvidercomputesisPublicRoutefrom route metadata and provides it via context - •
AuthWrapper(inside RouterProvider) usesuseRouter()to getisPublicRoute - •Public routes render immediately without waiting for auth validation
- •AuthWrapper re-renders on navigation, ensuring auth is checked when navigating to protected routes
Route path naming conventions:
- •Use kebab-case for route paths (e.g.,
/new-route, not/newRoute) - •Keep paths descriptive but concise
- •Avoid deep nesting when possible
3. Add Navigation Item
Update the navigation items in src/client/components/NavLinks.tsx to include your new route:
import { Extension } from 'lucide-react'; // Choose an appropriate icon
export const navItems: NavItem[] = [
{ path: '/', label: 'Home', icon: <Home /> },
{ path: '/ai-chat', label: 'AI Chat', icon: <MessageSquare /> },
{ path: '/file-manager', label: 'Files', icon: <Folder /> },
{ path: '/new-route', label: 'New Route', icon: <Extension /> }, // Add your new route here
{ path: '/settings', label: 'Settings', icon: <Settings /> },
];
Navigation item guidelines:
- •Choose a descriptive but concise label
- •Select an appropriate icon from lucide-react that represents the route's purpose
- •Consider the order of items in the navigation (most important/frequently used routes should be more accessible)
Using the Router
Navigation
To navigate between routes in your components, use the useRouter hook:
import { useRouter } from '../../router';
const MyComponent = () => {
const { navigate } = useRouter();
const handleClick = () => {
navigate('/new-route');
};
// You can also replace the current history entry
const handleReplace = () => {
navigate('/new-route', { replace: true });
};
return (
<Button onClick={handleClick}>Go to New Route</Button>
);
};
Navigation Guidelines
- •Always use the navigation API from useRouter: Never use
window.location.hreffor navigation as it causes a full page reload and breaks the SPA behavior.
// ❌ Don't do this
window.location.href = '/some-route';
// ✅ Do this instead
const { navigate } = useRouter();
navigate('/some-route');
- •This ensures consistent navigation behavior throughout the application
- •Preserves the SPA (Single Page Application) experience
- •Maintains application state during navigation
- •Enables proper history management
Navigating with Parameters
When navigating to routes that require parameters (like IDs), construct the path with the parameters included:
// Navigating to a route with a parameter
const { navigate } = useRouter();
// Navigate to a video page with a specific video ID
const handleVideoClick = (videoId) => {
navigate(`/video/${videoId}`);
};
Getting Current Route
You can access the current route path using the useRouter hook:
import { useRouter } from '../../router';
const MyComponent = () => {
const { currentPath } = useRouter();
return (
<div>
<p>Current path: {currentPath}</p>
</div>
);
};
Advanced Routing Features
Route Parameters
Our router automatically parses route parameters from the URL path. To define a route with parameters, use the colon syntax in your route path:
// In src/client/routes/index.ts
export const routes = createRoutes({
// Other routes...
'/items/:id': ItemDetail,
});
Then access the parameters in your component using the useRouter hook:
// src/client/routes/ItemDetail/ItemDetail.tsx
import { useRouter } from '../../router';
export const ItemDetail = () => {
const { routeParams } = useRouter();
const itemId = routeParams.id;
return (
<div>
<h1>Item Detail</h1>
{itemId ? <p>Item ID: {itemId}</p> : <p>Invalid item ID</p>}
</div>
);
};
Query Parameters
The router also automatically parses query parameters from the URL. Access them in your component using the useRouter hook:
// src/client/routes/SearchResults/SearchResults.tsx
import { useRouter } from '../../router';
export const SearchResults = () => {
const { queryParams } = useRouter();
const searchQuery = queryParams.q || '';
return (
<div>
<h1>Search Results</h1>
<p>Query: {searchQuery}</p>
</div>
);
};
PWA Route Persistence
The router automatically persists the current route to useUIStore (Zustand with localStorage).
How it works:
- •When navigating, the route is saved to
lastRoutein the UI store - •On app startup, if the URL is
/(root), the last route is restored - •Auth-related routes (
/login,/register, etc.) are NOT persisted
Excluded routes (not persisted):
- •
/login - •
/register - •
/logout - •
/forgot-password
This enables PWA instant boot - when iOS kills the app, reopening it returns to the last viewed page.