How the Instatic Admin Router Replaces react-router-dom: Custom Routing for the Admin Shell
The Instatic admin router replaces react-router-dom with a lightweight, query-string-only routing system implemented in src/admin/lib/routing/Router.tsx and routerHooks.ts, providing a compatible API through React context while avoiding external dependencies and full page reloads.
The CoreBunch/Instatic repository implements a custom in-house routing solution for its administrative interface. Instead of bundling the popular react-router-dom library, the project maintains a tiny replacement located in src/admin/lib/routing/ that manipulates only the query string, keeping URLs stable while enabling seamless internal navigation.
The Architecture Decision
The admin shell intentionally avoids react-router-dom to reduce bundle size and eliminate compatibility concerns. According to the source comments in src/admin/lib/routing/index.ts, this is described as a "Tiny in-house router for the admin app. Replaces react-router-dom for the admin shell." The router is built from scratch to integrate tightly with Instatic’s state management and permission systems, offering a minimal surface area that covers only the navigation needs of the admin panel.
Core Implementation Files
The routing system is split across three primary files to separate concerns between the router engine and its React API.
Router.tsx: The Root Component
Located at src/admin/lib/routing/Router.tsx, this file exports the core <Router> component. The component listens to browser popstate events and drives navigation by providing a context that tracks the current location. Unlike traditional routers that manipulate the full URL pathname, this implementation restricts changes to the query string, leaving the base path untouched.
routerHooks.ts: The API Surface
The src/admin/lib/routing/routerHooks.ts file implements the hook interface that mirrors react-router-dom. It exports useNavigate, useLocation, useParams, and a Link component. These hooks read from the context provided by <Router>, allowing the rest of the admin codebase to navigate programmatically and declaratively without importing external routing libraries.
index.ts: Public Interface
src/admin/lib/routing/index.ts serves as the public entry point, re-exporting the <Router> component and all hooks. The file header explicitly documents the module's purpose: internal admin navigation should use this router instead of any external routing solution.
Query-String-Only Navigation Strategy
The router's defining characteristic is its exclusive use of the query string for state management. When navigating, the router calls window.history.replaceState or pushState to update only the search parameters (e.g., ?page=123), never the pathname. This design prevents full page reloads while keeping the admin UI's URL stable, which is essential for maintaining the shell's state during internal navigation.
API Compatibility with react-router-dom
Despite being a custom implementation, the router exposes a familiar API to minimize the learning curve for developers accustomed to react-router-dom:
useNavigate– Returns a function to programmatically update the query string.useLocation– Provides access to the current location object, wherelocation.searchcontains the active query parameters.useParams– Extracts route parameters from the query string.Link– A component that renders anchor tags triggering query-string navigation without reloading the page.
Implementation Examples
The following patterns demonstrate how the admin codebase utilizes the custom router.
Wiring the router into the admin application:
// src/admin/App.tsx
import { Router } from '@admin/lib/routing';
import { AdminRoutes } from './routes';
export default function AdminApp() {
return (
<Router>
<AdminRoutes />
</Router>
);
}
Navigating programmatically from a panel component:
// src/admin/pages/site/SomePanel.tsx
import { useNavigate } from '@admin/lib/routing';
export function SomePanel() {
const navigate = useNavigate();
const openEditor = (pageId: string) => {
// Updates only the query string, e.g. ?page=123
navigate({ search: `?page=${pageId}` });
};
return <button onClick={() => openEditor('abc')}>Edit page</button>;
}
Using the Link component for declarative navigation:
// src/admin/components/SidebarLink.tsx
import { Link } from '@admin/lib/routing';
export function SidebarLink({ to, children }: { to: string; children: React.ReactNode }) {
// `to` is a query string like "?page=home"
return <Link to={to}>{children}</Link>;
}
Reading the current route state from a custom hook:
// src/admin/pages/site/hooks/useCurrentPage.ts
import { useLocation } from '@admin/lib/routing';
export function useCurrentPage() {
const location = useLocation();
// location.search holds the query string used by the admin router
const params = new URLSearchParams(location.search);
return params.get('page');
}
Preventing react-router-dom Imports
To ensure the custom router remains the sole navigation method for the admin interface, the project includes an architectural guard at src/__tests__/architecture/admin-router-usage.test.ts. This test suite verifies that no files in the admin codebase import from react-router-dom, enforcing the architectural decision at build time.
Summary
- The Instatic admin router is a custom-built replacement for
react-router-domlocated insrc/admin/lib/routing/. - It splits functionality between
Router.tsx(context provider) androuterHooks.ts(React hooks). - Navigation is restricted to query-string changes only, avoiding pathname manipulation and full page reloads.
- The API mirrors
react-router-domwithuseNavigate,useLocation,useParams, andLink. - An architecture test prevents accidental imports of
react-router-domthroughout the admin codebase.
Frequently Asked Questions
Why does Instatic use a custom router instead of react-router-dom?
Instatic implements a custom router to minimize bundle size and avoid the overhead of a full-featured routing library for the admin shell's specific needs. The custom solution is tightly integrated with the application's state management and permission systems, providing only the functionality required for internal navigation.
How does the Instatic router handle navigation without changing the pathname?
The router exclusively manipulates the query string using the History API (window.history.replaceState or pushState). When you call navigate({ search: '?page=123' }), it updates only the search portion of the URL, leaving the pathname intact. This prevents full page reloads while allowing the application to track internal state via URL parameters.
Is the custom router API compatible with react-router-dom hooks?
Yes, the custom router intentionally exposes a compatible API surface. Functions like useNavigate, useLocation, and useParams, along with the Link component, follow the same patterns and signatures as their react-router-dom counterparts, allowing developers familiar with that library to work immediately with Instatic's routing system.
How does the project prevent accidental imports of react-router-dom?
The repository includes a dedicated architecture test at src/__tests__/architecture/admin-router-usage.test.ts that scans the admin codebase and fails if any file imports from react-router-dom. This automated enforcement ensures the architectural boundary remains intact as the codebase evolves.
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 →