@shadow-js/router
v0.1.0
Published
Minimal, type-safe router for ShadowJS
Maintainers
Readme
ShadowJS Router
Declarative client-side routing for ShadowJS applications with support for nested routes, layouts, transitions, and scroll restoration.
✨ Features
- 🎯 Declarative Routing: Define routes using JSX components
- 🧩 Nested Routes: Support for nested layouts and route hierarchies
- 🎭 Transitions: Built-in route transition animations
- 📜 Scroll Restoration: Automatic scroll position management
- 🔗 Programmatic Navigation: Navigate programmatically with full control
- 🎨 Layout Support: Shared layouts for multiple routes
- 📱 Hash & History Modes: Choose between hash and history-based routing
- 🔍 Route Parameters: Dynamic route segments with type safety
📦 Installation
The ShadowJS Router requires @shadow-js/core as a peer dependency:
# Install both packages together
npm install @shadow-js/router @shadow-js/core
# Or if you already have @shadow-js/core installed
npm install @shadow-js/routerImportant: This router is specifically designed for ShadowJS applications and requires @shadow-js/core to function properly. The router imports directly from @shadow-js/core and will not work without it.
Development Setup
For development and testing, you can use the development configuration:
# Copy the development package.json
cp package.json.dev package.json
# Install dependencies (includes @shadow-js/core)
npm installPeer Dependency Architecture
This package uses a peer dependency approach for several reasons:
- Framework Integration: Tightly coupled with ShadowJS reactive system
- Bundle Size: Avoids bundling framework code that consumers already have
- Version Alignment: Ensures consumers use compatible versions
- Tree Shaking: Allows better optimization in consumer applications
🚀 Quick Start
Basic Routing
import { Router, Route, A, useLocation } from "@shadow-js/router";
function Navigation() {
return (
<nav>
<A href="/">Home</A>
<A href="/about">About</A>
<A href="/contact">Contact</A>
</nav>
);
}
function Home() {
const location = useLocation();
return (
<div>
<h1>Home</h1>
<p>Current path: {() => location.pathname}</p>
</div>
);
}
function About() {
return <h1>About Us</h1>;
}
function Contact() {
return <h1>Contact Us</h1>;
}
function NotFound() {
return <h1>404 - Page Not Found</h1>;
}
function App() {
return (
<Router notFound={NotFound}>
<Navigation />
<Route path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/contact" component={Contact} />
</Router>
);
}Dynamic Routes with Parameters
import { useParams } from "@shadow-js/router";
function UserProfile() {
const params = useParams();
return (
<div>
<h1>User Profile</h1>
<p>User ID: {() => params().id}</p>
</div>
);
}
function App() {
return (
<Router>
<Route path="/users/:id" component={UserProfile} />
</Router>
);
}🎯 Core Concepts
Route Configuration
Routes can be configured in two ways:
1. Declarative Routes (JSX)
<Router>
<Route path="/" component={Home} />
<Route path="/about" component={About} />
<Route path="/users/:id" component={UserProfile} />
</Router>2. Programmatic Routes (Objects)
const routes = [
{ path: "/", component: Home },
{ path: "/about", component: About },
{ path: "/users/:id", component: UserProfile },
];
<Router routes={routes} />;Route Matching
ShadowJS Router uses a sophisticated matching algorithm:
- Exact Matching:
/usersmatches only/users - Parameter Matching:
/users/:idmatches/users/123 - Wildcard Matching:
/users/*matches/users/123/posts - Optional Parameters:
/users/:id?matches/usersand/users/123
Nested Routes and Layouts
function DashboardLayout({ children }) {
return (
<div className="dashboard">
<nav>
<A href="/dashboard">Overview</A>
<A href="/dashboard/users">Users</A>
<A href="/dashboard/settings">Settings</A>
</nav>
<main>{children}</main>
</div>
);
}
function DashboardOverview() {
return <h2>Dashboard Overview</h2>;
}
function App() {
return (
<Router>
<Route path="/dashboard" layout={DashboardLayout}>
<Route path="/" component={DashboardOverview} />
<Route path="/users" component={DashboardUsers} />
<Route path="/settings" component={DashboardSettings} />
</Route>
</Router>
);
}📚 API Reference
Components
<Router>
The main router component that manages client-side routing for your application.
Props:
routes?: RouteConfig[]- Array of route configurations (alternative to children)children?: any- Child routes when using JSX syntaxtransition?: Transition- Default transition for all routesnotFound?: ComponentType- Component to render for unmatched routesmode?: "history" | "hash"- URL handling mode (default: "history")scroll?: ScrollBehavior- Scroll restoration behavior
Examples:
// JSX syntax
<Router notFound={NotFound} mode="history">
<Route path="/" component={Home} />
<Route path="/about" component={About} />
</Router>;
// Object syntax
const routes = [
{ path: "/", component: Home },
{ path: "/about", component: About },
];
<Router routes={routes} />;<Route>
Defines a route with its path and component.
Props:
path: string- Route path patterncomponent?: ComponentType- Component to render for this routelayout?: ComponentType- Layout component wrapping this routechildren?: RouteConfig[]- Nested routesredirect?: string | Function- Redirect path or functiontransition?: Transition- Route-specific transition
Examples:
// Basic route
<Route path="/home" component={Home} />
// Route with layout
<Route path="/dashboard" layout={DashboardLayout}>
<Route path="/" component={Overview} />
<Route path="/users" component={Users} />
</Route>
// Route with redirect
<Route path="/old-path" redirect="/new-path" />
// Dynamic route
<Route path="/users/:id" component={UserProfile} /><A>
Enhanced anchor tag for client-side navigation with automatic active states.
Props:
href: string- Navigation targetreplace?: boolean- Replace current history entrystate?: any- State to pass to the new routeclassName?: string | Function- CSS class (can be reactive)- All standard
<a>tag props
Examples:
<A href="/home">Home</A>
<A href="/users" className={() => (location.pathname === "/users" ? "active" : "")}>
Users
</A><Redirect>
Programmatically redirects to another route.
Props:
to: string- Redirect target path
Examples:
<Redirect to="/login" />Hooks
useLocation()
Returns the current location object with reactive properties.
Returns: Store<Location>
Location Properties:
pathname: string- Current pathsearch: string- Query stringhash: string- Hash fragmentstate: any- Navigation state
Examples:
function CurrentPage() {
const location = useLocation();
return (
<div>
<p>Current path: {location.pathname}</p>
<p>Search: {location.search}</p>
<p>Hash: {location.hash}</p>
</div>
);
}useParams<T>()
Returns route parameters extracted from the current URL.
Returns: Store<T | undefined>
Examples:
function UserProfile() {
const params = useParams<{ id: string }>();
return <div>User ID: {params().id}</div>;
}useSearchParams()
Manages URL search parameters with reactive updates.
Returns: [Store<URLSearchParams>, SetterFunction]
Examples:
function SearchComponent() {
const [searchParams, setSearchParams] = useSearchParams();
const updateSearch = (query: string) => {
setSearchParams({ q: query, page: "1" });
};
return (
<input
value={searchParams().get("q") || ""}
onInput={(e) => updateSearch(e.target.value)}
/>
);
}Functions
navigate(to, options?)
Programmatically navigate to a new route.
Parameters:
to: string- Target pathoptions?: NavigateOptions- Navigation options
NavigateOptions:
replace?: boolean- Replace current history entrystate?: any- State to pass to the new route
Examples:
// Basic navigation
navigate("/dashboard");
// Replace current history entry
navigate("/login", { replace: true });
// Pass state
navigate("/checkout", { state: { fromCart: true } });redirect(to)
Redirect to a new route (equivalent to navigate with replace: true).
Parameters:
to: string- Redirect target
Examples:
redirect("/login");Types
RouteConfig<P, T>
Configuration object for defining routes programmatically.
Properties:
path: P- Route path patterncomponent?: ComponentType- Route componentlayout?: ComponentType- Layout componentchildren?: RouteConfig[]- Nested routesredirect?: string | Function- Redirect configurationtransition?: Transition- Route transition
PathParams<P>
Type-safe route parameters extracted from path patterns.
Examples:
// For path "/users/:id/posts/:postId"
// PathParams = { id: string, postId: string }NavigateOptions<T>
Options for programmatic navigation.
Properties:
replace?: boolean- Replace history entrystate?: T- Navigation state
Transition
Animation effect for route changes.
Types:
"none"- No transition"fade"- Fade transitionCustomTransition- Custom transition function
🔧 Advanced Usage
Programmatic Navigation
import { navigate, redirect } from "@shadow-js/router";
function LoginForm() {
const [isLoggedIn, setIsLoggedIn] = useStore(false);
const handleLogin = () => {
setIsLoggedIn(true);
// Navigate after successful login
navigate("/dashboard", { replace: true });
};
const handleLogout = () => {
setIsLoggedIn(false);
// Redirect to home
redirect("/");
};
return (
<div>
<button onClick={handleLogin}>Login</button>
<button onClick={handleLogout}>Logout</button>
</div>
);
}Route Transitions
<Router transition="fade">
<Route path="/" component={Home} />
<Route path="/about" component={About} transition="slide" />
</Router>Available transitions: "fade", "slide", "scale", "none"
Custom Scroll Behavior
<Router
scroll={(context) => {
if (context.action === "PUSH") {
window.scrollTo(0, 0); // Scroll to top on navigation
} else if (context.action === "POP") {
// Restore scroll on browser back/forward
// Default behavior handles this automatically
}
}}
>
{/* routes */}
</Router>Route Guards and Redirects
function ProtectedRoute({ component: Component }) {
const [isAuthenticated, setIsAuthenticated] = useStore(false);
return (
<Show when={() => isAuthenticated()} fallback={<Redirect to="/login" />}>
<Component />
</Show>
);
}
function App() {
return (
<Router>
<Route
path="/dashboard"
component={() => <ProtectedRoute component={Dashboard} />}
/>
</Router>
);
}Query Parameters
import { useSearchParams } from "@shadow-js/router";
function ProductList() {
const [searchParams, setSearchParams] = useSearchParams();
const category = () => searchParams().get("category") || "all";
const sort = () => searchParams().get("sort") || "name";
const updateFilters = (newFilters) => {
setSearchParams({
...Object.fromEntries(searchParams().entries()),
...newFilters,
});
};
return (
<div>
<select
value={() => category()}
onChange={(e) => updateFilters({ category: e.target.value })}
>
<option value="all">All Categories</option>
<option value="electronics">Electronics</option>
<option value="books">Books</option>
</select>
</div>
);
}🏗️ Router Architecture
Route Compilation
The router compiles route configurations into an optimized matching tree:
- Parse route patterns into regex and parameter names
- Build matching tree for efficient route resolution
- Cache compiled routes for performance
Navigation Flow
- URL Change Detection: Listen to
popstateandhashchangeevents - Route Matching: Find matching route for current path
- Component Resolution: Resolve component and layout hierarchy
- Transition Handling: Apply route transitions
- Scroll Management: Handle scroll restoration
- Render: Update DOM with new route content
State Management
The router manages several reactive states:
- Current location: pathname, search, hash
- Current route match: matched route and parameters
- Navigation history: for back/forward functionality
- Scroll positions: for restoration
🔌 Integration
With ShadowJS
import { useStore } from "@shadow-js/core";
import { Router, Route, useLocation } from "@shadow-js/router";
function App() {
const [user, setUser] = useStore(null);
return (
<Router>
<Route path="/" component={Home} />
<Route path="/profile" component={Profile} />
</Router>
);
}Custom History Implementation
// For server-side rendering or custom history management
const customHistory = {
push: (path) => {
/* custom push logic */
},
replace: (path) => {
/* custom replace logic */
},
go: (delta) => {
/* custom go logic */
},
};
// Pass to router
<Router history={customHistory}>{/* routes */}</Router>;🎨 Styling Routes
Routes can have their own styling:
// Route-specific styles
<Route path="/dashboard" component={Dashboard} className="dashboard-route" />;
// Conditional styling based on active route
function Navigation() {
const location = useLocation();
return (
<nav>
<A href="/" className={() => (location.pathname === "/" ? "active" : "")}>
Home
</A>
<A
href="/about"
className={() => (location.pathname === "/about" ? "active" : "")}
>
About
</A>
</nav>
);
}🐛 Error Handling
Route Errors
function App() {
return (
<Router
notFound={() => <div>Custom 404 Page</div>}
onError={(error) => {
console.error("Route error:", error);
// Handle route errors
}}
>
{/* routes */}
</Router>
);
}Navigation Errors
import { navigate } from "@shadow-js/router";
try {
await navigate("/protected-route");
} catch (error) {
if (error.code === "UNAUTHORIZED") {
navigate("/login");
}
}📊 Performance
Route Preloading
// Preload routes on hover for better UX
function Link({ href, children }) {
const [isPreloading, setIsPreloading] = useStore(false);
return (
<A
href={href}
onMouseEnter={() => {
setIsPreloading(true);
// Preload route component
import(`./pages${href}.js`);
}}
className={() => (isPreloading() ? "preloading" : "")}
>
{children}
</A>
);
}Route Memoization
Routes automatically memoize components to prevent unnecessary re-renders.
🧪 Testing
import { render } from "@shadow-js/core";
import { Router, Route } from "@shadow-js/router";
describe("Routing", () => {
test("renders correct component for route", () => {
const container = document.createElement("div");
render(
<Router>
<Route path="/test" component={() => <div>Test Component</div>} />
</Router>,
container
);
// Navigate to test route
window.history.pushState({}, "", "/test");
expect(container.innerHTML).toContain("Test Component");
});
});📚 Examples
See the examples documentation for:
- Basic routing patterns
- Nested routes and layouts
- Dynamic routing
- Protected routes
- Search and query parameters
🤝 Contributing
We welcome contributions! See the Contributing Guide for details.
📄 License
MIT License - see LICENSE for details.
📞 Support
- Documentation: Router API
- Examples: Routing Examples
- Issues: GitHub Issues
- Discussions: GitHub Discussions
Built with ❤️ for the ShadowJS ecosystem.
