Skip to content

ShadowJS Router - TypeScript-first routing with JSX syntax, reactive state integration, and modern build tooling

License

Notifications You must be signed in to change notification settings

shadowjs-dev/router

ShadowJS Router

npm version TypeScript License: MIT

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/router

Important: 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 install

Peer Dependency Architecture

This package uses a peer dependency approach for several reasons:

  1. Framework Integration: Tightly coupled with ShadowJS reactive system
  2. Bundle Size: Avoids bundling framework code that consumers already have
  3. Version Alignment: Ensures consumers use compatible versions
  4. 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: /users matches only /users
  • Parameter Matching: /users/:id matches /users/123
  • Wildcard Matching: /users/* matches /users/123/posts
  • Optional Parameters: /users/:id? matches /users and /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 syntax
  • transition?: Transition - Default transition for all routes
  • notFound?: ComponentType - Component to render for unmatched routes
  • mode?: "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 pattern
  • component?: ComponentType - Component to render for this route
  • layout?: ComponentType - Layout component wrapping this route
  • children?: RouteConfig[] - Nested routes
  • redirect?: string | Function - Redirect path or function
  • transition?: 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 target
  • replace?: boolean - Replace current history entry
  • state?: any - State to pass to the new route
  • className?: 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 path
  • search: string - Query string
  • hash: string - Hash fragment
  • state: 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 path
  • options?: NavigateOptions - Navigation options

NavigateOptions:

  • replace?: boolean - Replace current history entry
  • state?: 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 pattern
  • component?: ComponentType - Route component
  • layout?: ComponentType - Layout component
  • children?: RouteConfig[] - Nested routes
  • redirect?: string | Function - Redirect configuration
  • transition?: 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 entry
  • state?: T - Navigation state

Transition

Animation effect for route changes.

Types:

  • "none" - No transition
  • "fade" - Fade transition
  • CustomTransition - 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:

  1. Parse route patterns into regex and parameter names
  2. Build matching tree for efficient route resolution
  3. Cache compiled routes for performance

Navigation Flow

  1. URL Change Detection: Listen to popstate and hashchange events
  2. Route Matching: Find matching route for current path
  3. Component Resolution: Resolve component and layout hierarchy
  4. Transition Handling: Apply route transitions
  5. Scroll Management: Handle scroll restoration
  6. 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


Built with ❀️ for the ShadowJS ecosystem.

About

ShadowJS Router - TypeScript-first routing with JSX syntax, reactive state integration, and modern build tooling

Resources

License

Code of conduct

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published