How the Instatic Admin Routing System Works Without react-router-dom

The Instatic admin routing system replaces react-router-dom with a lightweight, custom SPA router located in src/admin/lib/routing/ that uses the History API, a custom instatic:locationchange event, and React contexts to handle navigation across the admin dashboard.

The CoreBunch/Instatic project implements a bespoke routing solution for its administrative interface that deliberately avoids external dependencies like react-router-dom. This custom Instatic admin routing system resides entirely within src/admin/lib/routing/ and provides a minimal, type-safe API for managing the /admin/* single-page application. By leveraging browser history APIs directly and synchronizing state through custom events, the router maintains a flat, predictable route structure while keeping bundle size minimal.

Core Components and Architecture

Router and MemoryRouter

The foundation of the system lives in src/admin/lib/routing/Router.tsx. The Router component registers a global popstate listener and intercepts calls to history.pushState and history.replaceState. When navigation occurs, it emits a custom instatic:locationchange event, ensuring all subscribers synchronize without triggering unnecessary re-renders. For testing environments, MemoryRouter provides an identical API but maintains an in-memory history stack instead of modifying the browser’s URL.

Routes and Route Matching

The Routes component iterates over its direct Route children in declaration order, selecting the first pattern that matches the current pathname. Each Route is declarative metadata only—it never renders itself. The matching logic relies on a matchPath function implemented in src/admin/lib/routing/routerHooks.ts that compiles patterns like /admin/:id or * wildcards into regular expressions. Because the system uses flat matching without nested route trees, route order determines precedence, requiring catch-all patterns like /admin/* to appear last.

Two components handle user navigation imperatively and declaratively:

  • Navigate: Renders nothing but pushes or replaces a location immediately upon mounting, useful for redirects.
  • Link: Renders a standard <a> element that intercepts left-clicks, invokes the router’s navigation function, and prevents full page reloads. It respects modifier keys, non-left clicks, and target="_blank" attributes by falling back to native navigation behavior.

Router Hooks

The src/admin/lib/routing/routerHooks.ts file exports essential hooks that consume contexts provided by Router or MemoryRouter:

  • useLocation: Returns the current location object.
  • useNavigate: Returns an imperative function to push or replace history entries.
  • useParams: Extracts URL parameters from matched routes.
  • useInRouterContext: Detects whether the component renders within a router context.

These hooks leverage useSyncExternalStore internally to subscribe to the instatic:locationchange event, ensuring UI updates always reflect the browser’s history state.

How Navigation Works

The Instatic admin routing system follows a synchronous, event-driven flow:

  1. Initialization: src/admin/main.tsx mounts the Router component around the <AdminRoutes/> application shell, installing listeners for popstate and instatic:locationchange events.
  2. Trigger: Calling navigate(to, {replace}) or clicking a Link invokes history.pushState or replaceState wrapped in React.startTransition.
  3. Broadcast: The router dispatches the custom instatic:locationchange event.
  4. Synchronization: Hooks subscribed via useSyncExternalStore detect the event, re-read window.location, and trigger React renders for components consuming useLocation or useParams.
  5. Rendering: The Routes component re-evaluates its children, calls matchPath against the new location, and renders the first matching element.

Implementation Examples

Setting Up the Router

The entry point wraps the admin application in the custom Router:

// src/admin/main.tsx
import { Router } from '@admin/lib/routing';
import { AdminRoutes } from './router';

export default function AdminApp() {
  return (
    <Router>
      <AdminRoutes />
    </Router>
  );
}

Defining Admin Routes

The route table uses declarative components similar to react-router but with stricter ordering requirements:

// src/admin/router.tsx
import { Routes, Route, Navigate } from '@admin/lib/routing';
import { AdminEntry } from './pages/AdminEntry';

export function AdminRoutes() {
  return (
    <Routes>
      <Route path="/" element={<Navigate to="/admin/dashboard" replace />} />
      <Route path="/admin/dashboard" element={<AdminEntry section="dashboard" />} />
      <Route path="/admin/site" element={<AdminEntry section="site" />} />
      {/* Workspace routes */}
      <Route path="/admin/*" element={<Navigate to="/admin/dashboard" replace />} />
    </Routes>
  );
}

Access routing functionality through the custom hooks:

import { useNavigate, useParams } from '@admin/lib/routing';

function ToolbarButton() {
  const navigate = useNavigate();
  return (
    <button onClick={() => navigate('/admin/media')}>
      Open Media
    </button>
  );
}

function PluginPage() {
  const { pluginId, pageId } = useParams<{ pluginId: string; pageId: string }>();
  return (
    <div>Plugin: {pluginId} – Page: {pageId}</div>
  );
}

Use the Link component for declarative navigation:

import { Link } from '@admin/lib/routing';

function NavItem() {
  return <Link to="/admin/content">Content</Link>;
}

Testing with MemoryRouter

For unit tests, replace Router with MemoryRouter to avoid browser history side effects:

import { MemoryRouter, Routes, Route } from '@admin/lib/routing';
import { render } from '@testing-library/react';

render(
  <MemoryRouter initialEntries={['/admin/dashboard']}>
    <Routes>
      <Route path="/admin/dashboard" element={<Dashboard />} />
    </Routes>
  </MemoryRouter>
);

Summary

  • The Instatic admin routing system lives entirely in src/admin/lib/routing/ and replaces react-router-dom with a zero-dependency implementation.
  • Router.tsx contains the core components (Router, MemoryRouter, Routes, Route, Link, Navigate) that manage history and rendering.
  • routerHooks.ts provides useLocation, useNavigate, useParams, and useInRouterContext backed by useSyncExternalStore.
  • Navigation relies on the History API and a custom instatic:locationchange event to synchronize all subscribers efficiently.
  • Pattern matching supports :param segments and * wildcards via regex compilation in matchPath, with route order determining precedence.
  • The system is fully type-safe and includes MemoryRouter for isolated testing environments.

Frequently Asked Questions

Why does Instatic avoid react-router-dom?

The CoreBunch/Instatic team built a custom router to minimize bundle size and eliminate unnecessary features for the admin SPA. By keeping the implementation in src/admin/lib/routing/, they maintain full control over the routing API while ensuring the history API remains the single source of truth.

How does the router handle URL parameters?

The matchPath function in src/admin/lib/routing/routerHooks.ts compiles route patterns containing :param segments into regular expressions. When a match occurs, it extracts parameters into an object accessible via the useParams() hook, which returns type-safe parameter values based on the route definition.

Can I use nested routes with this system?

No, the Instatic admin routing system deliberately uses a flat route structure. The Routes component only examines its direct children, and there is no support for nested layout routes or outlet rendering. Route precedence depends entirely on declaration order, so specific routes must precede catch-all patterns like /admin/*.

What happens if I use router hooks outside of a Router component?

Hooks like useLocation, useNavigate, and useParams detect whether they are rendered within a router context using useInRouterContext. If called outside the Router or MemoryRouter tree, they throw an error to prevent undefined behavior, similar to react-router’s error handling.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →