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.
Navigation Components
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, andtarget="_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:
- Initialization:
src/admin/main.tsxmounts theRoutercomponent around the<AdminRoutes/>application shell, installing listeners forpopstateandinstatic:locationchangeevents. - Trigger: Calling
navigate(to, {replace})or clicking aLinkinvokeshistory.pushStateorreplaceStatewrapped inReact.startTransition. - Broadcast: The router dispatches the custom
instatic:locationchangeevent. - Synchronization: Hooks subscribed via
useSyncExternalStoredetect the event, re-readwindow.location, and trigger React renders for components consuminguseLocationoruseParams. - Rendering: The
Routescomponent re-evaluates its children, callsmatchPathagainst the new location, and renders the first matchingelement.
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>
);
}
Navigation and Parameters
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, anduseInRouterContextbacked byuseSyncExternalStore. - Navigation relies on the History API and a custom
instatic:locationchangeevent to synchronize all subscribers efficiently. - Pattern matching supports
:paramsegments and*wildcards via regex compilation inmatchPath, with route order determining precedence. - The system is fully type-safe and includes
MemoryRouterfor 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →