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

> Discover Instatic's lightweight admin router, a purpose-built react-router-dom replacement. Learn its context-based API, popstate listeners, and type-safe hooks like useNavigate and useParams.

- Repository: [CoreBunch/Instatic](https://github.com/CoreBunch/Instatic)
- Tags: architecture
- Published: 2026-07-26

---

**The Instatic admin router is a lightweight, purpose-built replacement for react-router-dom that lives in [`src/admin/lib/routing/Router.tsx`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/main.tsx):

```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`](https://github.com/CoreBunch/Instatic/blob/main/Router.tsx).

### The Hook API in routerHooks.ts

Navigation functionality is exposed through [`src/admin/lib/routing/routerHooks.ts`](https://github.com/CoreBunch/Instatic/blob/main/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:

```tsx
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:

```tsx
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:

- **[`src/admin/lib/routing/Router.tsx`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/lib/routing/Router.tsx)**: Contains the core `<Router>` component, the `RouterContext` definition, and the `popstate` event subscription logic.

- **[`src/admin/lib/routing/routerHooks.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/lib/routing/routerHooks.ts)**: Implements `useNavigate`, `useLocation`, `useParams`, and additional hooks that consume the router context.

- **[`src/admin/lib/routing/index.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/lib/routing/index.ts)**: Barrel file that re-exports the public API, providing a clean import surface for the rest of the admin codebase.

- **[`src/admin/main.tsx`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/main.tsx)**: Entry point that mounts the custom `<Router>` at the top level, replacing any traditional browser router implementation.

- **[`src/admin/lib/urlState/urlState.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/lib/urlState/urlState.ts)**: Utility file that works alongside the router to manage query-string-only changes without triggering route matching when unnecessary.

## 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`](https://github.com/CoreBunch/Instatic/blob/main/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:

```tsx
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`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/lib/routing/Router.tsx) and [`routerHooks.ts`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/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.