# How Instatic's Custom Admin Router Replaces react-router-dom: A 150-Line Alternative

> Discover how Instatic's custom admin router replaces react-router-dom with only 150 lines of code. Shrink your admin bundle by 30 KB and maintain API compatibility effortlessly.

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

---

**Instatic eliminates `react-router-dom` from its admin UI by implementing a ~150-line custom router in `src/admin/lib/routing` that uses `history.pushState` and a custom `instatic:locationchange` event, reducing the admin bundle by approximately 30 KB while maintaining API compatibility through `useSyncExternalStore` and `React.startTransition`.**

Instatic replaces the industry-standard `react-router-dom` library with a purpose-built admin router optimized for its four-route single-page application. This custom implementation lives under `src/admin/lib/routing` and delivers a negligible bundle footprint compared to the ~30 KB gzipped cost of the full routing library. By stripping away unused features like nested routes and data loaders, Instatic achieves faster cold-load performance while preserving a familiar React Router API surface.

## Why Instatic Replaced react-router-dom

The decision to drop `react-router-dom` centers on bundle size and feature elimination. The full library ships with sophisticated capabilities—including loaders, actions, nested layouts, and data routers—that Instatic's admin UI never utilizes. According to the CoreBunch/Instatic source code, importing `react-router-dom` would inflate the eager bundle by roughly 30 KB gzipped, while the custom admin router weighs in at approximately 150 lines of code with negligible impact on the final bundle size.

This lean approach targets the admin SPA's specific requirements: a flat route table handling only four to ten static paths. By avoiding the overhead of general-purpose routing logic, Instatic ensures ultra-fast cold-load performance for administrative interfaces.

## API Surface and Component Parity

Despite the dramatic size reduction, Instatic's admin router maintains API compatibility with `react-router-dom`'s essential components. In [`src/admin/lib/routing/Router.tsx`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/lib/routing/Router.tsx), the implementation exposes familiar primitives including **`Router`**, **`MemoryRouter`**, **`Routes`**, **`Route`**, **`Navigate`**, and **`Link`**.

The hook implementations in [`src/admin/lib/routing/routerHooks.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/lib/routing/routerHooks.ts) provide **`useLocation`**, **`useNavigate`**, **`useParams`**, and **`useInRouterContext`**, ensuring that developers can write routing logic using the same patterns as standard React Router applications. This surface-level parity allows for intuitive adoption without learning a new API paradigm.

Mount the router at the admin entry point as follows:

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

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

```

## Custom Navigation Implementation

Unlike `react-router-dom`'s internal state management, Instatic's router relies on native browser APIs. Navigation occurs through **`history.pushState`** and **`history.replaceState`**, complemented by a custom **`instatic:locationchange`** event dispatched whenever the location updates.

Components subscribe to location changes using **`useSyncExternalStore`**, which reads the current location from the browser's history state without triggering full React re-render loops. This approach, implemented in [`src/admin/lib/routing/Router.tsx`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/lib/routing/Router.tsx), provides efficient synchronization between the URL and React component tree while maintaining compatibility with React 18 concurrent features.

## Smooth Transitions with React.startTransition

A critical performance advantage over standard `react-router-dom` behavior involves transition handling. Instatic wraps every navigation call in **`React.startTransition`**, allowing the UI to continue displaying the previous page while lazy-loaded admin workspace chunks download in the background.

This prevents the flashing `<Suspense>` fallback that typically occurs during route transitions in standard React Router applications. When navigating between admin sections, users experience seamless transitions rather than jarring loading states, as the router coordinates with React's concurrent rendering to prioritize user experience.

For programmatic navigation, use the admin-specific hook:

```tsx
import { useAdminNavigate } from '@admin/lib/useAdminNavigate';

function SaveButton() {
  const navigate = useAdminNavigate();

  const handleSave = async () => {
    await saveData();
    navigate('/admin/content');
  };

  return <button onClick={handleSave}>Save</button>;
}

```

## Architectural Constraints and Route Limitations

The admin router deliberately imposes strict limitations to maintain its minimal footprint. It supports only **flat route tables**—nested routes are explicitly prohibited. Route matching recognizes static segments, **`:param`** placeholders, and a catch-all **`*`** wildcard, but disallows optional segments and regex-style patterns.

These constraints, documented in [`docs/reference/admin-router.md`](https://github.com/CoreBunch/Instatic/blob/main/docs/reference/admin-router.md), prevent accidental complexity while satisfying the admin UI's fixed route set. The router is encapsulated behind the **`@admin/lib/routing`** barrel export, with strict rules forbidding imports from core or module code to ensure routing logic remains confined to the admin interface.

## Defining Routes and Testing

Route definitions follow a familiar JSX pattern similar to `react-router-dom`, as implemented in [`src/admin/router.tsx`](https://github.com/CoreBunch/Instatic/blob/main/src/admin/router.tsx):

```tsx
import { Routes, Route, Navigate } from '@admin/lib/routing';
import AdminEntry from './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/*" element={<Navigate to="/admin/dashboard" replace />} />
    </Routes>
  );
}

```

For testing scenarios, the **`MemoryRouter`** component simulates navigation without manipulating the real browser history:

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

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

expect(screen.getByText('Dashboard')).toBeInTheDocument();

```

## Summary

- Instatic's custom admin router replaces `react-router-dom` with a ~150-line implementation located in `src/admin/lib/routing`, eliminating ~30 KB from the admin bundle.
- The router uses **`history.pushState`** and a custom **`instatic:locationchange`** event for navigation, subscribed via **`useSyncExternalStore`**.
- Every navigation is wrapped in **`React.startTransition`** to enable smooth lazy-loaded transitions without `<Suspense>` fallback flashing.
- The API surface mirrors `react-router-dom` with components like **`Router`**, **`Routes`**, **`Route`**, and hooks like **`useNavigate`**, but supports only flat routes with static segments, `:param` placeholders, and `*` wildcards.
- Strict encapsulation behind **`@admin/lib/routing`** prevents usage outside the admin UI, ensuring architectural boundaries remain intact.

## Frequently Asked Questions

### What specific react-router-dom features does Instatic's router omit?

Instatic's implementation intentionally omits nested route configurations, data loaders, actions, and regex-based path matching. It supports only flat route tables with static segments, `:param` placeholders, and catch-all `*` wildcards. This limitation keeps the codebase at approximately 150 lines while satisfying the admin SPA's simple routing requirements.

### How does the custom router handle browser history without react-router-dom?

The router manipulates browser history directly through the native **`history.pushState`** and **`history.replaceState`** APIs. When navigation occurs, it dispatches a custom **`instatic:locationchange`** event that notifies all subscribed components. Hooks like **`useLocation`** leverage **`useSyncExternalStore`** to read from this external history source, ensuring synchronization with React's rendering cycle without the overhead of React Router's internal state machine.

### Can I use the Instatic admin router in non-admin parts of the application?

No. The admin router is strictly encapsulated behind the **`@admin/lib/routing`** barrel export and is forbidden in core or module code according to the project architecture rules defined in [`docs/reference/admin-router.md`](https://github.com/CoreBunch/Instatic/blob/main/docs/reference/admin-router.md). This constraint ensures that the admin-specific routing logic remains isolated and prevents accidental coupling between the admin interface and other application modules.

### Why does Instatic use React.startTransition for navigation?

Instatic wraps navigation calls in **`React.startTransition`** to prioritize keeping the current UI visible while loading lazy-loaded route components in the background. This prevents the jarring flash of a `<Suspense>` fallback that typically occurs during route transitions in standard `react-router-dom` applications, creating a smoother user experience when navigating between admin sections with heavy asynchronous dependencies.