Skip to content

Repository files navigation

@page-speed/router

Lightweight, SSR-compatible routing for the OpenSite website platform. Native browser routing with zero dependencies, tree-shakable exports, and < 3KB gzipped.

Opensite Semantic UI


npm version npm downloads TypeScript

Features

  • 🚀 Ultra-lightweight: < 3KB minified + gzipped
  • 🔥 SSR-safe: Built for server-side rendering with proper hydration
  • 🌲 Tree-shakable: Import only what you need
  • 🎯 Native browser APIs: Uses History API and window.location
  • 📦 Zero dependencies: Only React as peer dependency
  • Performance-first: Optimized for Core Web Vitals
  • 🎨 TypeScript: Full type safety
  • 🧩 Modular: Hooks work independently, provider optional

Installation

npm install @page-speed/router
# or
pnpm add @page-speed/router
# or
yarn add @page-speed/router

Quick Start

Basic Navigation (No Provider Required)

Many hooks work without any provider setup:

import { useNavigation, useUrl } from '@page-speed/router';

function App() {
  const { navigateTo } = useNavigation();
  const { pathname } = useUrl();

  return (
    <div>
      <p>Current path: {pathname}</p>
      <button onClick={() => navigateTo('/about')}>
        Go to About
      </button>
    </div>
  );
}

With RouterProvider (For Dynamic Params)

import { RouterProvider, useParams } from '@page-speed/router';

const routes = [
  { path: '/' },
  { path: '/blog/:slug' },
  { path: '/products/:category/:id' }
];

function App() {
  return (
    <RouterProvider routes={routes}>
      <BlogPost />
    </RouterProvider>
  );
}

function BlogPost() {
  const { slug } = useParams();
  return <div>Blog post: {slug}</div>;
}

Core Hooks

useUrl()

Get current URL information. Works without RouterProvider.

const {
  href,       // Full URL
  pathname,   // Path without query/hash
  search,     // Query string
  hash,       // Hash fragment
  origin,     // Protocol + host
  host,       // Hostname + port
  hostname,   // Just hostname
  port,       // Port number
  protocol    // Protocol (http:, https:)
} = useUrl();

useNavigation()

Programmatic navigation. Works without RouterProvider.

const { navigateTo, replace, reload } = useNavigation();

// Navigate to path
navigateTo('/about');

// Navigate with options
navigateTo({
  path: '/blog',
  anchor: 'comments',  // Smooth scroll to anchor
  replace: false,      // Use pushState (default)
  smooth: true,        // Smooth scrolling (default)
  state: { from: 'home' }  // History state
});

// Replace current entry
replace('/new-path');

// Reload page
reload();

useGoBack()

Safe back navigation with fallback.

const { goBack, canGoBack } = useGoBack({
  fallback: '/home'  // Where to go if no history
});

// Go back or fallback
goBack();

// Go back multiple steps
goBack(-2);

// Conditional UI
<button disabled={!canGoBack}>Back</button>

useParams()

Extract dynamic route parameters. Requires RouterProvider with routes.

// Route: /blog/:slug
// URL: /blog/hello-world

const params = useParams();
// { slug: 'hello-world' }

// Include query params
const allParams = useParams(true);
// { slug: 'hello-world', page: '2' }

useRouteMatch()

Check if current path matches a pattern.

const match = useRouteMatch('/blog/:slug');

if (match.isMatch) {
  console.log(match.params); // { slug: 'current-slug' }
}

Utility Hooks

usePathname()

const pathname = usePathname(); // '/blog/post-1'

useSearchParams()

const params = useSearchParams();
// URL: /products?category=electronics&sort=price
// Returns: { category: 'electronics', sort: 'price' }

const repeated = useSearchParams();
// URL: /products?tag=news&tag=events&page=2
// Returns: { tag: ['news', 'events'], page: '2' }

useHash()

const hash = useHash();
// URL: /blog#comments
// Returns: 'comments' (without #)

useUpdateSearchParams()

const updateParams = useUpdateSearchParams();

// Add/update params
updateParams({ page: '2', sort: 'date' });

// Replace one key with repeated values (unrelated params are retained)
updateParams({ tag: ['news', 'events'] });

// Remove param
updateParams({ category: null });

// Replace history
updateParams({ page: '3' }, true);

SSR Setup

Rails Integration

<!-- customer_websites/chai_index.html.erb -->
<script>
  window.__ROUTER_INITIAL_STATE__ = {
    path: '<%= @initial_path %>',
    params: <%= @initial_params.to_json %>
  };
</script>
<div id="root"></div>
// Client entry point
import { RouterProvider } from '@page-speed/router';

const initialState = window.__ROUTER_INITIAL_STATE__;

ReactDOM.hydrateRoot(
  document.getElementById('root'),
  <RouterProvider
    initialPath={initialState.path}
    routes={routes}
  >
    <App />
  </RouterProvider>
);

Next.js Integration

// _app.tsx
import { RouterProvider } from '@page-speed/router';

export default function App({ Component, pageProps, router }) {
  return (
    <RouterProvider initialPath={router.pathname}>
      <Component {...pageProps} />
    </RouterProvider>
  );
}

Anchor Navigation

Automatic smooth scrolling to anchors:

const { navigateTo } = useNavigation();

// Navigate to page and scroll to anchor
navigateTo({
  path: '/docs',
  anchor: 'installation'
});

// Scroll to anchor on current page
navigateTo({ anchor: 'features' });

// Control scroll behavior
navigateTo({
  path: '/about',
  anchor: 'team',
  smooth: false  // Instant scroll
});

Advanced Usage

Custom Route Matching

import { matchPath, buildPath } from '@page-speed/router';

// Match a path
const match = matchPath('/blog/hello', '/blog/:slug');
// { isMatch: true, params: { slug: 'hello' }, path: '/blog/hello' }

// Build a path from pattern
const path = buildPath('/blog/:slug', { slug: 'world' });
// '/blog/world'

Multiple Route Patterns

import { useMultiMatch } from '@page-speed/router';

const match = useMultiMatch([
  { pattern: '/blog/:slug' },
  { pattern: '/products/:id' },
  { pattern: '/', exact: true }
]);

if (match?.path.startsWith('/blog')) {
  // Handle blog routes
}

Navigation Events

<RouterProvider
  onNavigate={(path) => {
    // Track page views
    analytics.track('page_view', { path });
  }}
>
  <App />
</RouterProvider>

Note: onNavigate only fires for the initial load when initialPath matches the current location. For reliable page-view tracking (initial load + every navigation), use the analytics hooks below.

Page View Analytics (Opt-In)

Fully optional — if you never import these, they are tree-shaken away and the router behaves exactly as before. Apps that use the router without analytics (showcases, internal tools) need no changes.

usePageViews() — bring your own backend

Fires once after mount and once per client-side navigation (routechange and browser back/forward). Consecutive duplicate paths are deduped, so the double event dispatched by navigateTo and hash-only changes never double-count.

import { usePageViews } from '@page-speed/router';

usePageViews(({ path, previousPath, isInitial }) => {
  myAnalytics.track('page_view', { path, isInitial });
});

usePageViewAnalytics() / <PageViewAnalytics /> — DashTrack analytics engine

Reports page views to POST /website_page_views on the DashTrack API.

import { RouterProvider, PageViewAnalytics } from '@page-speed/router';

<RouterProvider>
  <PageViewAnalytics websiteToken={website.token} />
  <App />
</RouterProvider>
Option Type Default Description
websiteToken string | null | undefined - The website's UUID token (not the numeric website id). Tracking no-ops when absent.
apiBaseUrl string https://api.dashtrack.com Analytics API origin
category string 'webpage' Page view category
enabled boolean true Set false to suspend tracking (e.g. in development)
transformPayload (payload, view) => payload | null - Inspect/extend the payload, or return null to skip a view

Delivery details: payload is nested under the website_page_view wrapper key, sent with keepalive: true and no credentials; failures are swallowed so analytics can never break the host site. The visitor IP is derived server-side from the request — no client-side IP lookup is performed.

API Reference

RouterProvider Props

Prop Type Default Description
children ReactNode - Child components
initialPath string - Initial path for SSR
routes Route[] [] Route patterns for param extraction
scrollBehavior 'smooth' | 'auto' 'smooth' Default scroll behavior
onNavigate (path: string) => void - Navigation callback

Route Object

interface Route {
  path: string;    // Pattern like '/blog/:slug'
  exact?: boolean; // Exact match required
}

Performance

  • Bundle Size: < 3KB minified + gzipped
  • Tree-shaking: Import individual hooks for smaller bundles
  • No Re-renders: Optimized context updates
  • Lazy Loading: Dynamic imports for code splitting
  • 60fps Scrolling: Smooth anchor navigation

Browser Support

  • Chrome/Edge 88+
  • Firefox 78+
  • Safari 14+
  • Chrome Android 88+
  • Safari iOS 14+

Requires History API and IntersectionObserver support.

Migration Guide

From React Router

// Before (React Router)
import { useNavigate, useParams } from 'react-router-dom';
const navigate = useNavigate();
navigate('/about');

// After (@page-speed/router)
import { useNavigation, useParams } from '@page-speed/router';
const { navigateTo } = useNavigation();
navigateTo('/about');

From Next.js Router

// Before (Next.js)
import { useRouter } from 'next/router';
const router = useRouter();
router.push('/about');

// After (@page-speed/router)
import { useNavigation } from '@page-speed/router';
const { navigateTo } = useNavigation();
navigateTo('/about');

TypeScript

Full TypeScript support with exported types:

import type {
  UrlState,
  NavigateOptions,
  RouteParams,
  Route
} from '@page-speed/router';

Contributing

See CONTRIBUTING.md for development setup and guidelines.

License

BSD-3-Clause

Support


Built with ❤️ for the OpenSite ecosystem

About

Lightweight, SSR-compatible routing for React web applications. Native browser routing with zero dependencies, tree-shakable exports, and < 3KB gzipped.

Topics

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages