How Instatic's Admin Router Replaces react-router-dom: Architecture and Implementation

The Instatic admin router is a lightweight, purpose-built replacement for react-router-dom that lives in src/admin/lib/routing/Router.tsx and provides a context-based routing API using popstate listeners, custom navigation events, and type-safe hooks like useNavigate and useParams.

The CoreBunch/Instatic repository implements a custom in-house routing layer specifically for its CMS administration interface. Unlike the public-facing site, the admin UI requires deterministic navigation that functions reliably inside sandboxed plugin environments, prompting the team to replace react-router-dom with a leaner solution located in src/admin/lib/routing/.

Why Replace react-router-dom?

The admin interface faces unique constraints that make react-router-dom unsuitable. The CMS runs plugins inside a QuickJS-WASM sandbox, meaning code cannot directly access the browser history API. Additionally, react-router-dom’s full history stack and dependency surface add unnecessary overhead for an application that only needs to push or replace the current URL without complex back-navigation support.

The custom router therefore implements only the essential subset required for CMS operations: a <Router> component listening to popstate and internal "replaceState" events, a <Link> component for declarative navigation, and a minimal hook API that avoids the memory pressure of maintaining a full navigation stack.

Core Architecture Components

The Router Component

At the heart of the system is the <Router> component exported from src/admin/lib/routing/Router.tsx. This component subscribes to native popstate events and an internal router-state emitter, creating a single source of truth for the current location.

The component wraps the entire admin application in src/admin/main.tsx:

import { Router } from '@admin/routing';
import { App } from './App';

export default function AdminRoot() {
  return (
    <Router>
      <App />
    </Router>
  );
}

Unlike react-router-dom’s BrowserRouter, this implementation does not maintain a history stack. It stores only the current location object, reducing memory footprint and eliminating complexity for the admin shell's navigation patterns.

Context-Based State Management

The router creates a RouterContext that exposes location, navigate, and match objects to the entire component tree. All child components consume this context through specialized hooks rather than importing external routing dependencies.

This design ensures that even when the UI manipulates URL query strings without triggering full navigation, the router state remains synchronized through the internal emitter mentioned in the source comments of Router.tsx.

The Hook API in routerHooks.ts

Navigation functionality is exposed through src/admin/lib/routing/routerHooks.ts, which exports useNavigate, useLocation, useParams, and other utilities. These hooks read from RouterContext and provide type-safe access to route parameters.

For example, accessing typed route parameters works as follows:

import { useParams } from '@admin/routing';

export function PageEditor() {
  const { pageId } = useParams<{ pageId: string }>();
  // pageId is type-safe thanks to the router's TypeScript validation
  return <Editor pageId={pageId} />;
}

Programmatic navigation uses the useNavigate hook:

import { useNavigate } from '@admin/routing';

export function NewPageButton() {
  const navigate = useNavigate();
  
  const goToNewPage = () => navigate('/admin/pages/new');
  return <Button onClick={goToNewPage}>New Page</Button>;
}

Key Design Decisions

The Instatic admin router makes several architectural tradeoffs to optimize for the CMS environment:

  • Browser-Router Split: The system listens to both native popstate events and custom internal events, allowing the router to handle URL query-string changes without triggering unnecessary re-renders or full navigation cycles.

  • No Full History Stack: Because the admin UI only performs push or replace operations (never requiring back-button management), the router stores just the current location object rather than maintaining a complete history stack.

  • Static Type Safety: Route patterns use TypeScript literals validated at match time, ensuring that useParams<T>() always returns the correct shape and preventing runtime parameter errors.

File Structure and Implementation Details

The routing system spans several files in the src/admin/lib/routing/ directory:

Preventing react-router-dom Usage

To ensure the architecture remains consistent, the repository includes an architecture gate test at src/__tests__/architecture/admin-router-usage.test.ts. This test asserts that no file within the admin codebase imports from react-router-dom, enforcing that all navigation uses the in-house router components and hooks.

The test validates that imports are restricted to the custom routing layer, preventing accidental introduction of the heavier dependency into the admin bundle.

Usage Examples

Declarative navigation uses the <Link> component, which mirrors react-router-dom’s API but forwards to the internal navigate function:

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

export function Sidebar() {
  return (
    <nav>
      <Link to="/admin/pages">Pages</Link>
      <Link to="/admin/media">Media Library</Link>
    </nav>
  );
}

The router integrates with the plugin sandbox by exposing a simple push/replace API through the SDK, allowing sandboxed plugins to request navigation without direct access to browser history APIs.

Summary

  • Instatic replaces react-router-dom in the admin UI with a custom router located in src/admin/lib/routing/Router.tsx and routerHooks.ts.

  • Context-based architecture provides useNavigate, useLocation, and useParams through a React context rather than a full routing library.

  • Sandbox-compatible design operates without direct browser history manipulation, making it safe for QuickJS-WASM plugin environments.

  • Architecture tests in admin-router-usage.test.ts enforce the prohibition against react-router-dom imports in the admin codebase.

  • Type-safe parameters ensure that route data is validated at compile time using TypeScript generics.

Frequently Asked Questions

Why did Instatic build a custom router instead of using react-router-dom?

The admin UI requires deterministic navigation that functions inside a sandboxed plugin environment where direct browser history API access is restricted. React-router-dom brings unnecessary complexity and bundle size for this specific use case, whereas the custom implementation provides only the push/replace functionality needed by the CMS while maintaining compatibility with the QuickJS-WASM sandbox.

How does the Instatic router handle browser events like the back button?

The router subscribes to native popstate events while also listening to an internal emitter for custom navigation actions. This dual-listening approach allows it to respond to standard browser navigation while also handling URL state changes initiated by the admin UI's internal query-string manipulations without full page transitions.

Can plugins use the admin router for navigation?

Yes, the router's simple API is safe to expose through the plugin SDK. Because plugins run in a QuickJS-WASM sandbox and cannot access window.history, the router abstracts navigation into a callable interface that plugins can invoke to request URL changes without violating sandbox security boundaries.

Is the Instatic admin router compatible with react-router-dom hooks?

While the hook names (useNavigate, useParams, useLocation) are familiar to react-router-dom users, they are distinct implementations tied to the custom RouterContext. Code using these hooks in the Instatic admin UI imports from @admin/routing rather than react-router-dom, ensuring complete separation between the two routing systems.

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 →