A modern React router built on the Navigation API.
- Navigation API based - Uses the modern Navigation API (supported by Chrome, Firefox and Safari 26.2+)
- Native
<a>tags just work - No special<Link>component needed; use standard HTML links - Object-based routes - Define routes as plain JavaScript objects
- Nested routing - Support for layouts and nested routes with
<Outlet> - Type-safe - Full TypeScript support
- Lightweight - Minimal API surface
- RSC Compatible - Designed to work also with React Server Components
npm install @funstack/routerThis is a pnpm monorepo. To set up the development environment:
# Install dependencies
pnpm install
# Build the router package
pnpm build
# Run the example app
pnpm --filter funstack-router-example dev
# Run tests
pnpm testpackages/router- The main@funstack/routerlibrarypackages/example- Example applicationpackages/docs- Documentation site
import { Router, Outlet, route } from "@funstack/router";
import type { RouteDefinition, RouteComponentProps } from "@funstack/router";
function Layout() {
return (
<div>
<nav>
{/* Native <a> tags work for client-side navigation */}
<a href="/">Home</a>
<a href="/users">Users</a>
</nav>
<Outlet />
</div>
);
}
function Home() {
return <h1>Home</h1>;
}
function Users() {
return <h1>Users</h1>;
}
function UserDetail({ params }: RouteComponentProps<{ id: string }>) {
return <h1>User {params.id}</h1>;
}
const routes: RouteDefinition[] = [
route({
path: "/",
component: Layout,
children: [
route({ path: "", component: Home }),
route({ path: "users", component: Users }),
route({ path: "users/:id", component: UserDetail }),
],
}),
];
function App() {
return <Router routes={routes} />;
}The root component that provides routing context.
<Router routes={routes} />| Prop | Type | Description |
|---|---|---|
routes |
RouteDefinition[] |
Array of route definitions |
onNavigate |
OnNavigateCallback |
Optional callback invoked before navigation is intercepted |
fallback |
FallbackMode |
Fallback mode when Navigation API is unavailable (default: "none") |
Renders the matched child route. Used in layout components.
function Layout() {
return (
<div>
<nav>...</nav>
<Outlet />
</div>
);
}Returns a function for programmatic navigation.
const navigate = useNavigate();
// Basic navigation
navigate("/users");
// With options
navigate("/users", { replace: true, state: { from: "home" } });Returns the current location.
const location = useLocation();
// { pathname: "/users", search: "?page=1", hash: "#section" }Returns typed route parameters for a route definition created with route() and an id. The route definition is used to infer the parameter types.
const userRoute = route({
id: "user",
path: "users/:userId",
component: UserPage,
});
function UserPage() {
const params = useRouteParams(userRoute);
// params is typed as { userId: string }
}Returns typed navigation state for a route definition. State is tied to the navigation history entry and persists across back/forward navigation.
type ScrollState = { scrollPos: number };
const scrollRoute = routeState<ScrollState>()({
id: "scroll",
path: "/scroll",
component: ScrollPage,
});
function ScrollPage() {
const state = useRouteState(scrollRoute);
// state is typed as ScrollState | undefined
}Returns typed loader data for a route definition.
const userRoute = route({
id: "user",
path: "users/:userId",
loader: async ({ params }) => fetchUser(params.userId),
component: UserPage,
});
function UserPage() {
const data = useRouteData(userRoute);
// data is typed based on the loader return type
}Returns and allows updating URL search parameters.
const [searchParams, setSearchParams] = useSearchParams();
// Read
const page = searchParams.get("page");
// Update
setSearchParams({ page: "2" });
// Update with function
setSearchParams((prev) => {
prev.set("page", "2");
return prev;
});Returns whether a navigation transition is currently pending.
const isPending = useIsPending();Blocks navigation away from the current route. Useful for scenarios like unsaved form data.
Note: This hook only handles SPA navigations. For hard navigations (tab close, refresh), handle beforeunload separately.
useBlocker({
shouldBlock: () => {
if (isDirty) {
return !confirm("You have unsaved changes. Leave anyway?");
}
return false;
},
});Helper function for creating type-safe route definitions. Path parameters are inferred from the path pattern.
import { route } from "@funstack/router";
// Route without loader
route({
path: "users/:id",
component: UserDetail, // receives RouteComponentProps<{ id: string }>
});
// Route with loader
route({
path: "users/:id",
loader: async ({ params, signal }) => fetchUser(params.id),
component: UserDetail, // receives RouteComponentPropsWithData<{ id: string }, User>
});
// Route with id (enables type-safe hooks like useRouteParams)
route({
id: "user",
path: "users/:id",
component: UserDetail,
});
// Pathless route (always matches, useful for layout wrappers)
route({
component: Layout,
children: [
/* ... */
],
});Route options:
| Option | Type | Description |
|---|---|---|
path |
string |
URL path pattern (e.g., "users/:id"). Omit for pathless routes |
component |
ComponentType | ReactNode |
Component to render or JSX element |
children |
RouteDefinition[] |
Nested child routes |
loader |
(args: LoaderArgs) => T |
Data loader function |
id |
string |
Route identifier for type-safe hooks |
exact |
boolean |
Override matching (default: exact for leaf, prefix for parent) |
requireChildren |
boolean |
Whether parent requires a child to match (default: true) |
Curried helper for creating routes with typed navigation state.
import { routeState } from "@funstack/router";
type FilterState = { filter: string };
const productRoute = routeState<FilterState>()({
id: "products",
path: "products",
loader: async () => fetchProducts(),
component: ProductList, // receives { data, params, state: FilterState | undefined, setState, ... }
});Props passed to route components without a loader.
interface RouteComponentProps<TParams, TState = undefined> {
params: TParams;
state: TState | undefined;
setState: (
state: TState | ((prev: TState | undefined) => TState),
) => Promise<void>;
setStateSync: (
state: TState | ((prev: TState | undefined) => TState),
) => void;
resetState: () => void;
info: unknown;
isPending: boolean;
}Props passed to route components with a loader. Extends RouteComponentProps with a data field.
interface RouteComponentPropsWithData<
TParams,
TData,
TState = undefined,
> extends RouteComponentProps<TParams, TState> {
data: TData;
}Arguments passed to loader functions.
type LoaderArgs<Params> = {
params: Params;
request: Request;
signal: AbortSignal;
};type Location = {
pathname: string;
search: string;
hash: string;
};type NavigateOptions = {
replace?: boolean;
state?: unknown;
info?: unknown;
};FUNSTACK Router uses the URLPattern API for path matching.
| Pattern | Example | Matches |
|---|---|---|
/users |
/users |
Exact match |
/users/:id |
/users/123 |
Named parameter |
/files/* |
/files/a/b/c |
Wildcard |
MIT
