How Instatic Admin Routing Differs from react-router-dom: A Custom Implementation Analysis
Instatic replaces react-router-dom with a ~150-line custom router located in src/admin/lib/routing that eliminates ~30 KB of gzipped bundle overhead while maintaining API compatibility for the admin SPA's flat route structure.
The CoreBunch/Instatic repository implements a purpose-built routing solution for its administrative interface. Unlike standard React applications that import the full react-router-dom package, Instatic's admin routing uses a lightweight alternative optimized for a fixed set of four to ten static routes. This custom implementation prioritizes cold-load performance and eliminates unused features that would otherwise inflate the JavaScript bundle.
Why Instatic Built a Custom Router Instead of Using react-router-dom
The decision to avoid react-router-dom stems from specific constraints of the admin UI's architecture and performance requirements.
Bundle Size and Performance Impact
react-router-dom ships with a comprehensive feature set including loaders, actions, nested layouts, and data routers that Instatic never uses. Importing the full library would add approximately 30 KB gzipped to every admin page load. The custom implementation in src/admin/lib/routing/Router.tsx compresses the entire routing logic into roughly 150 lines of code, keeping the admin's eager bundle minimal and improving cold-start performance.
Feature Set Mismatch
The admin router deliberately implements only the features required for the admin UI's flat structure. It supports static segments, :param placeholders, and a catch-all * wildcard, but explicitly disallows optional segments, nested routes, and regex-style matching. This constraint prevents accidental route-matching complexity and ensures the implementation remains tiny.
Core Architectural Differences
The Instatic admin router differs from react-router-dom in its state management, navigation mechanics, and rendering behavior.
Custom Navigation Events and State Management
Instead of relying on React's internal state for location changes, the admin router uses history.pushState and history.replaceState paired with a custom instatic:locationchange event. This event notifies all subscribed components that the location changed, allowing useSyncExternalStore to re-read the location without triggering a full React re-render loop. This approach differs from react-router-dom's internal state management and reduces re-render overhead.
Smooth Transitions with React.startTransition
Every navigation call in src/admin/lib/routing/Router.tsx is wrapped in React.startTransition. When a lazy-loaded admin workspace chunk is still loading, the UI continues displaying the previous page instead of flashing a <Suspense> fallback. This behavior provides smoother transitions than the default react-router-dom implementation, which may show loading states immediately upon navigation.
Flat Route Table Structure
Unlike react-router-dom, which supports deeply nested route configurations, Instatic's admin router enforces a flat route table. The implementation matches the fixed set of admin paths without recursive route matching, simplifying the code and reducing runtime overhead.
Implementation Details from the Source Code
The router implementation spans several key files under the @admin/lib/routing barrel export.
Router.tsx Core Components
The file src/admin/lib/routing/Router.tsx exports the primary components: Router, MemoryRouter, Routes, Route, Navigate, and Link. These mirror the react-router-dom API surface but with streamlined implementations. The MemoryRouter variant enables testing without a real DOM history, useful for unit tests that need to control the initial route state.
routerHooks.ts and Path Matching
The src/admin/lib/routing/routerHooks.ts file implements the hook layer (useLocation, useNavigate, useParams, useInRouterContext) and the path-matching logic. The matching algorithm supports only three patterns: static strings, colon-prefixed parameters (:param), and the asterisk wildcard (*). This limited grammar keeps the matching function small and predictable compared to the complex pattern matching in react-router-dom.
Encapsulation and Import Constraints
The router is strictly encapsulated behind the @admin/lib/routing barrel. Core application code and module code must never import these routing internals directly. This constraint, documented in docs/reference/admin-router.md, ensures the routing logic remains confined to the admin UI and prevents accidental dependencies in other parts of the codebase.
Usage Examples: Instatic vs. react-router-dom
Mounting the Router
In a typical react-router-dom application, you would import BrowserRouter or Router from the package. In Instatic, the admin entry point in src/admin/main.tsx uses the custom implementation:
import { Router } from '@admin/lib/routing';
import { AdminRoutes } from './router';
function AdminApp() {
return (
<Router>
<AdminRoutes />
</Router>
);
}
Defining Routes
The route definition syntax remains familiar but uses the admin-specific imports:
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/media" element={<AdminEntry section="media" />} />
<Route path="/admin/*" element={<Navigate to="/admin/dashboard" replace />} />
</Routes>
);
}
Programmatic Navigation
While react-router-dom provides useNavigate, Instatic offers useAdminNavigate from src/admin/lib/useAdminNavigate.ts for programmatic navigation with view-transition effects:
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>;
}
Testing with MemoryRouter
Testing remains compatible with React Testing Library using the custom MemoryRouter:
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();
Comparison Summary
| Aspect | Instatic Admin Router | react-router-dom |
|---|---|---|
| Bundle size | ~150 lines, negligible gz | ~30 KB gz (full library) |
| Feature set | Flat routes, :param, *, no data loaders |
Full feature set (loaders, actions, nested routes) |
| Navigation mechanism | history.pushState + custom instatic:locationchange event |
Internal state management + history |
| Transition handling | startTransition wraps every navigation for smooth lazy-load |
No built-in transition handling |
| Route complexity | Static and param only, no nesting | Supports nested, dynamic, and complex routes |
| Import constraints | Only @admin/lib/routing may import; forbidden in core |
Imported freely throughout app |
Summary
- Instatic's admin routing replaces
react-router-domwith a ~150-line custom implementation to eliminate ~30 KB of bundle overhead. - The router exposes a compatible API surface (
Router,Routes,Route,Link,useLocation,useNavigate) but uses custominstatic:locationchangeevents andhistory.pushStatefor navigation. - Every navigation is wrapped in
React.startTransitionto prevent Suspense fallbacks during lazy loading, providing smoother transitions than standardreact-router-dom. - The implementation supports only flat routes with static segments,
:paramplaceholders, and*wildcards, deliberately excluding nested routes and complex matching patterns. - All routing code is encapsulated in
src/admin/lib/routingand must not be imported by core or module code outside the admin UI.
Frequently Asked Questions
Does Instatic's admin router support nested routes like react-router-dom?
No, the admin router intentionally does not support nested routes. According to the implementation in src/admin/lib/routing/routerHooks.ts, the router only handles flat route tables with static segments, :param placeholders, and * wildcards. This limitation keeps the implementation small and prevents the complexity of recursive route matching, which is unnecessary for the admin UI's fixed four-to-ten route structure.
How does the custom router handle navigation without re-rendering the entire component tree?
The router uses history.pushState and history.replaceState combined with a custom instatic:locationchange event. Components subscribe to location changes using useSyncExternalStore, which reads the current location from the history state without requiring a React context update or full re-render. This approach differs from react-router-dom's context-based updates and reduces render overhead during navigation.
Can I use react-router-dom hooks like useSearchParams with Instatic's router?
No, the admin router does not implement useSearchParams or other data-router features from react-router-dom. The available hooks are limited to useLocation, useNavigate, useParams, and useInRouterContext as defined in src/admin/lib/routing/routerHooks.ts. If query parameter handling is required, you must parse window.location.search manually or use the useLocation hook to access the raw URL.
Why does the admin router wrap navigation in React.startTransition?
The React.startTransition wrapper ensures that when navigating to a route with lazy-loaded components, React keeps the previous UI visible until the new chunk loads. This prevents the Suspense fallback from flashing between pages, creating a smoother user experience. According to the source in src/admin/lib/routing/Router.tsx, this is applied automatically to every navigation call, unlike react-router-dom where you would need to manually implement such transitions.
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 →