Router
Anchor Router is a strongly-typed React application router that doesn't ask you to install React Router for routing, React Query for data fetching, Zustand for state management, and write custom <PrivateRoute> wrappers for access control. Instead of gluing multiple libraries together, it builds all of this directly into a single, fully-typed route object:
const profileRoute = usersRoute
.route('/:user_id')
.guard(() => {
if (!isAuthenticated()) throw redirect(loginRoute);
})
.provide('profile', ({ params }) => {
return getUserProfile(params.user_id);
})
.provide('notifications', ({ params }) => {
return getUserNotifications(params.user_id);
})
.render((state) => {
const ProfileCard = template(() => (
<div>
<h1>{state.data?.profile?.name}</h1>
<p>{state.data?.profile?.email}</p>
</div>
));
const Notifications = template(() => (
<ul>
{state.data?.notifications?.map((notification) => (
<li key={notification.id}>{notification.message}</li>
))}
</ul>
));
return (
<>
<ProfileCard />
<Notifications />
</>
);
});
export const Profile = route(profileRoute);What's happening here
Route Nesting
In Anchor, routes are trees. The profile route chains from usersRoute (which defines /users) and adds /:user_id, making the full URL /users/:user_id. The URL path, data loaders, access control, and the component view all live in the exact same chain.
What it solves:
- Disconnected route configs requiring you to jump between
router.tsxandProfile.tsxjust to see the URL - Silent bugs during refactoring because you renamed a path segment but forgot to update child route configurations
- Messy URL path concatenation for deeply nested routes
Route Protection
Anchor handles route protection through guards. A guard is simply a function that determines whether a route is allowed to activate. Crucially, guards run out-of-band during the navigation phase—before React renders anything. If a guard rejects the navigation, the component never mounts and the providers never fetch. Guards can be used for any access check, such as authentication, feature flags, or subscription tiers.
What it solves:
- "Flashes" of unauthorized layout before an auth
useEffectfinally triggers and redirects - Component trees buried under five levels of
<RequireAuth>,<RequireFeature>, and<RequireRole>wrappers - Mixing access control logic directly inside UI render functions
Data Loading
Data loading is handled by providers, which also execute entirely during the routing phase. You can chain as many providers as you need. They execute sequentially, and each provider receives the fully resolved data from the previous ones. Because the providers resolve before rendering, the component receives the resulting state.data.profile and state.data.notifications immediately upon mounting. Additionally, because providers run inside a reactive observer, reading global state inside a provider automatically triggers a re-fetch when that state changes.
What it solves:
- The
useEffect→useState→if (loading)boilerplate in every single route component - "Waterfall" dependency fetching where a child component can't start fetching until the parent finishes rendering
- Prop drilling data from a high-level route component all the way down to a deeply nested child
- Flashing loading spinners because data fetching starts after the UI renders
- Writing messy
enabled: !!parentDataflags in React Query just to sequence dependent API calls - Piping global state into UI components for the sole purpose of passing it into a
queryKeyarray - Stale data bugs because you forgot to track an external state change in a React Query or
useEffectdependency array - Installing React Query or SWR just to handle cache and fetch states
View
Because Anchor resolves guards and providers entirely outside the React lifecycle, the .render() function represents pure UI. Because the route is a single chain, TypeScript instantly infers the exact shape of your data—giving you perfect autocomplete for state.data.profile and state.data.notifications.
By wrapping your components in the template() HOC, you opt into fine-grained reactivity. The injected state is observable—when a specific piece of data updates in the background, only the exact DOM node reading that data re-renders, not the entire page.
What it solves:
- Installing Zustand or Redux just to share data between sibling components (like
ProfileCardandNotifications) - Manually writing TypeScript interfaces for APIs and casting data blindly inside the component
- Top-down virtual DOM diffing where a minor data update forces the whole route to re-render
- Explicitly setting up
useMemoandReact.memoto stop React from re-rendering the whole tree
Navigation
The resulting Profile export is both a renderable React component and a deeply-typed link target. You pass the route object itself to <Link> instead of a path string:
<Link to={Profile} params={{ user_id: '42' }}>View Profile</Link>Anchor resolves the final URL dynamically at runtime. If you change /:user_id to /:id in the route definition, the URL automatically updates across every link in the app. TypeScript enforces that new id is provided in params at compile time.
What it solves:
- Hardcoded string-based
<Link to="/users/42">that breaks silently when paths are restructured - Finding broken links in production instead of catching them at build time
- Missing or mistyped URL parameters causing runtime 404s
What's the difference
| Without Anchor | With Anchor |
|---|---|
| Install React Router for routing | Built into the route tree |
| Install React Query / SWR for data fetching and caching | .provide('key', fetchFn) on the route |
| Install Zustand or Redux for global state management | Route state is inherently reactive via template() |
Write if (!x) return <Navigate /> in every component for auth, feature flags, subscriptions | .guard(checkFn) on the route — runs before anything renders |
| Write useState + useEffect for loading/error states in every component | state.status and state.data managed by the router |
Write string paths in <Link to="/users/42"> that break on rename | <Link to={Profile} params={\{ user_id: '42' \}}> — typed, refactor-safe |
Getting started
Create a router and mount it:
// lib/router.ts
import { createRouter, RENDER_MODE, MAX_AGE } from '@anchorlib/react/router';
import type { ReactNode } from 'react';
export const router = createRouter<ReactNode>({
renderMode: RENDER_MODE.IMMEDIATE,
maxAge: MAX_AGE.DAY,
});// main.tsx
import { UIRouter } from '@anchorlib/react/router';
import { router } from './lib/router.js';
import AppRoot from './routes/Index.js';
createRoot(document.body).render(
<UIRouter router={router} root={AppRoot} />
);RENDER_MODE.IMMEDIATE mounts the component before providers finish — use state.status to show loading UI. The default (DEFERRED) waits for all providers to resolve. MAX_AGE.DAY caches provider results for 24 hours so returning to a route skips the fetch.
Route tree
Each file exports a route segment. Children import from parents:
// routes/route.ts
import { router } from '../lib/router.js';
export const rootRoute = router.route('/');
// routes/users/route.ts
import { rootRoute } from '../route.js';
export const usersRoute = rootRoute.route('/users');
// routes/users/profile/route.ts
import { usersRoute } from '../route.js';
export const profileRoute = usersRoute.route('/:user_id');Layout routes receive children as the third argument to .render():
// routes/users/Index.tsx
import { route } from '@anchorlib/react/router';
import { usersRoute } from './route.js';
usersRoute
.provide('meta', async () => ({ title: 'All Users' }))
.render((_state, _ctx, children) => (
<div>
<header>Users</header>
{children}
</div>
));
export default route(usersRoute);When the URL is /users, {children} contains the index route. Navigate to /users/42, and only {children} swaps to the profile — the layout stays mounted.
Learn more
- Routes & Layouts — route tree, index routes, render API, route state, and options
- Navigation —
<Link>,navigate(), active state, and preloading - Data Loaders —
.provide(), caching, retry, and reactive re-evaluation - Guards —
.guard(),redirect(), error handling, and route protection