Routing and Navigation
Because AppShell manages routing internally, it owns the application's RouterProvider instance.
To use client-side navigation from AppShell pages, use the exports from @tailor-platform/app-shell, such as the Link component or useParams / useNavigate hooks, instead of importing those primitives from your app's react-router dependency. This keeps every hook and component on the same router context; mixing AppShell with a separate react-router import can fail at runtime, especially across mismatched major versions.
Exported React Router Hooks
AppShell re-exports the following hooks and components from react-router for use in your components:
useLocation- Access the current location objectuseNavigate- Programmatic navigationuseParams- Access route parametersuseSearchParams- Access and manipulate URL search parametersuseRouteError- Access error details in error boundariesLink- Client-side navigation componentNavigate- Declarative redirect component
Example Usage
import { useNavigate, useParams, useLocation, Link } from "@tailor-platform/app-shell";
const MyComponent = () => {
const navigate = useNavigate();
const { id } = useParams();
const location = useLocation();
const handleClick = () => {
// Navigate programmatically
navigate("/dashboard/overview");
};
return (
<div>
<p>Current path: {location.pathname}</p>
<p>Route param ID: {id}</p>
{/* Client-side link */}
<Link to="/products">View Products</Link>
<button onClick={handleClick}>Go to Dashboard</button>
</div>
);
};Declarative Redirects
Navigate redirects as a render result, which avoids the useEffect + navigate() pattern (that pattern renders the old page for a frame before it redirects):
import { Navigate, useAppShellData } from "@tailor-platform/app-shell";
const AdminPage = () => {
const { currentUser } = useAppShellData();
if (currentUser?.role !== "admin") {
return <Navigate to="/dashboard" replace />;
}
return <AdminDashboard />;
};Pass replace when the redirect should not leave the abandoned route in history — otherwise the browser Back button lands the user right back on it, and bounces them forward again.
Choosing between Navigate and redirectTo()
Both redirect, but they run at different points:
| Runs | Use for | |
|---|---|---|
redirectTo() | Route guard, before the component mounts | Route-level access control — the preferred option when the decision can be made from guard context |
Navigate | During render, from inside a component | Decisions that depend on component state, hooks, or fetched data |
Reach for redirectTo() first: it never mounts the component. Navigate is the fallback for cases a guard cannot express — notably WithGuard, which does not support redirectTo().
Breadcrumbs
AppShell automatically generates breadcrumbs from your module and resource hierarchy. Each path segment corresponds to a breadcrumb item, using the title (or meta.breadcrumbTitle) defined in defineModule / defineResource.
Static Breadcrumb Titles
Set a fixed breadcrumb title via meta.breadcrumbTitle:
defineResource({
path: ":id",
meta: {
breadcrumbTitle: (segment) => `Order #${segment}`,
},
component: OrderDetailPage,
});
// Breadcrumb shows: "Orders > Order #12345"Dynamic Breadcrumb Titles
Use the useOverrideBreadcrumb hook to replace a breadcrumb segment with a data-driven value from within the rendered page component:
import { useOverrideBreadcrumb } from "@tailor-platform/app-shell";
defineResource({
path: ":id",
component: () => {
const { data } = useQuery(GET_ORDER, { variables: { id } });
// Breadcrumb updates reactively once data loads
useOverrideBreadcrumb(data?.order?.name);
return <OrderDetail />;
},
});While title is undefined (e.g., loading), the override is cleared and the static title is shown. The override is automatically cleaned up on unmount.
See useOverrideBreadcrumb for the full API reference.
Command Palette for Quick Navigation
AppShell includes a CommandPalette component that provides keyboard-driven quick navigation to any page in your application.
Features
- Keyboard Shortcut:
Cmd+K(Mac) orCtrl+K(Windows/Linux) - Fuzzy Search: Search by page title or path
- Hierarchical Display: Shows module > resource breadcrumbs
- Keyboard Navigation: Use arrow keys and Enter to navigate
- Multilingual: Supports English and Japanese locales
Setup
The CommandPalette is built into AppShell and rendered automatically:
import { AppShell, SidebarLayout } from "@tailor-platform/app-shell";
const App = () => (
<AppShell modules={modules} locale="en">
<SidebarLayout />
</AppShell>
);The CommandPalette automatically:
- Collects all navigable routes from your module definitions
- Respects guards (modules/resources returning
hidden()won't appear) - Updates when navigation items change
- Adapts to the current locale
User Experience
- User presses
Cmd+K/Ctrl+Kanywhere in the app - Command palette dialog opens with fuzzy search
- User types to filter pages (e.g., "order detail")
- Navigate results with arrow keys
- Press Enter to navigate to selected page
No configuration needed - it just works!
Type-Safe Navigation with Generated Routes
When using file-based routing with the vite-plugin, you can enable automatic generation of type-safe route helpers. This provides compile-time checking for route paths and their parameters.
Setup
Enable generateTypedRoutes in your vite config:
// vite.config.ts
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import { appShellRoutes } from "@tailor-platform/app-shell-vite-plugin";
export default defineConfig({
plugins: [
react(),
appShellRoutes({
pagesDir: "src/pages",
// Enable with default output path ("src/routes.generated.ts")
generateTypedRoutes: true,
// Or customize output path:
// generateTypedRoutes: { output: "src/my-routes.ts" },
}),
],
});This generates a src/routes.generated.ts file containing type definitions for all your routes.
Generated File
The generated file exports a paths helper with a type-safe for() method:
// src/routes.generated.ts (auto-generated)
import { createTypedPaths } from "@tailor-platform/app-shell";
type RouteParams = {
"/": {};
"/dashboard": {};
"/orders": {};
"/orders/:id": { id: string };
"/orders/:orderId/items/:itemId": { orderId: string; itemId: string };
};
export const paths = createTypedPaths<RouteParams>();
export type { RouteParams };Usage
import { useNavigate } from "@tailor-platform/app-shell";
import { paths } from "./routes.generated";
const MyComponent = () => {
const navigate = useNavigate();
// ✅ Static route - no params needed
const goToDashboard = () => {
navigate(paths.for("/dashboard"));
};
// ✅ Dynamic route - params required and type-checked
const goToOrder = (orderId: string) => {
navigate(paths.for("/orders/:id", { id: orderId }));
};
// ✅ Multiple params
const goToOrderItem = (orderId: string, itemId: string) => {
navigate(paths.for("/orders/:orderId/items/:itemId", { orderId, itemId }));
};
// ✅ Query string passthrough
const goToOrderWithTab = (orderId: string) => {
navigate(paths.for("/orders/:id?tab=details", { id: orderId }));
};
// ✅ Dynamic query values via template literal
const goToOrderWithDynamicQuery = (orderId: string, tab: string) => {
navigate(paths.for(`/orders/:id?tab=${tab}`, { id: orderId }));
};
// ❌ TypeScript error: missing required params
// navigate(paths.for("/orders/:id"));
// ❌ TypeScript error: invalid path
// navigate(paths.for("/invalid/path"));
return <button onClick={goToDashboard}>Go to Dashboard</button>;
};Opt-In Design
This feature is opt-in. If you don't enable generateTypedRoutes, you can continue building paths dynamically:
// Still works without typed routes
navigate(`/orders/${orderId}`);HMR Support
The generated file is automatically regenerated when:
- A new
page.tsxis added - A
page.tsxis deleted - The dev server starts