sinwan-router
v1.0.1
Published
Client-side router for Sinwan apps — declarative route matching, params, lazy loading, and prefetching built on Sinwan's reactivity system.
Maintainers
Readme
Sinwan Router matches paths with a radix tree (no regex compilation), keeps route state in signals, and remounts only the outlet depth that actually changed.
Why Sinwan Router?
- Radix-tree matching — static segments,
:params, and*wildcardsin O(depth). Static routes win over params; params win over catch-alls. - Fine-grained reactivity —
path,matched,params, andsearchParamsare signals/computeds. Navigation updates the DOM that reads them. - Nested outlets — parent layouts stay mounted when only a child param changes. Key identity is depth-local, not the full URL.
- Lazy routes + prefetch —
() => import("./Page")orlazy(...).Linkprefetch andRouterOutletshare one per-router cache. - Browser-safe links —
Linkleaves_blank,download, modifier clicks (ctrl/meta/shift/alt), external URLs, and same-page hashes to the browser.
Peer dependency: sinwan >= 1.3.0 (<Key> remounts when cache is omitted).
Install
npm install sinwan-router sinwanbun add sinwan-router sinwanJSX still comes from Sinwan:
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "sinwan",
},
}Quick Start
import { cc, provide } from "sinwan/component";
import {
createRouter,
RouterKey,
RouterOutlet,
Link,
NavLink,
} from "sinwan-router";
const router = createRouter([
{ path: "/", component: Home },
{ path: "/users/:id", component: UserProfile },
{ path: "/about", component: () => import("./About.tsx") },
]);
const App = cc(() => {
provide(RouterKey, router);
return (
<div>
<nav>
<Link href="/">Home</Link>
<NavLink href="/about" activeClass="active">
About
</NavLink>
</nav>
<RouterOutlet />
</div>
);
});There is no RouterProvider component. Provide the instance with provide(RouterKey, router) so hooks and outlet components can inject it. Or declare routes in JSX with <Routes> / <Route> — same fields as RouteDefinition.
JSX routes
<Route> uses the native route fields: path, component, children, and meta. Nested <Route> elements become children on the parent definition.
import { Routes, Route, Link } from "sinwan-router";
const App = cc(() => (
<Routes>
<nav>
<Link href="/">Home</Link>
<Link href="/users/1">Profile</Link>
</nav>
<Route path="/" component={Home} />
<Route path="/users/:id" component={UserProfile} />
<Route path="/app" component={AppLayout} meta={{ title: "App" }}>
<Route path="/" component={AppIndex} />
<Route path="settings" component={Settings} />
</Route>
</Routes>
));Routes collects <Route> children into a router, provides RouterKey to layout siblings (Link, NavLink), and renders a <RouterOutlet />. Put links inside <Routes> — they need that context. You can still pass fallback, notFound, error, and initialPath.
Routes
import { createRouter, lazy } from "sinwan-router";
const router = createRouter([
{ path: "/", component: Home },
{ path: "/users/:id", component: UserProfile },
{ path: "/files/*path", component: Files },
{
path: "/app",
component: AppLayout,
children: [
{ path: "/", component: AppIndex },
{ path: "settings", component: Settings },
],
},
{ path: "/*", component: NotFound },
]);/:id— named param, URI-decoded, available fromuseParams()./*or/*path— catch-all. Unnamed wildcards land inparams._wildcard.- Nested
childrenjoin onto the parent path. A childpath: "/"is an index route. - Route components are not passed params as props. Read them with hooks.
Zero-arg factories (() => import("./Page")) are treated as lazy loaders. Functions that take props are treated as components. You can also brand a loader:
{ path: "/admin", component: lazy(() => import("./Admin.tsx")) }Navigation
router.navigate("/users/42");
router.navigate("/login", { replace: true, state: { from: router.path } });
router.prefetch("/about");
router.dispose();prefetch and RouterOutlet share the same instance cache. dispose() removes the popstate listener and drops that cache.
<Link href="/users/1" class="nav">
Profile
</Link>
<Link href="https://sinwanjs.com" target="_blank" rel="noreferrer">
Docs
</Link>
<NavLink href="/users" activeClass="active" class="nav">
Users
</NavLink>NavLink is active on an exact match or a nested prefix (/users matches /users/42). Prefetch on hover is on by default; pass prefetch={false} to disable.
Outlet
<RouterOutlet />
<RouterOutlet depth={1} fallback={Loading} notFound={NotFound} error={LoadError} />| Prop | Role |
| ----------- | -------------------------------------------------------------------- |
| depth | 0 is the root match; 1 is the first nested child, and so on |
| fallback | Shown while a lazy import is in flight |
| notFound | Shown at depth 0 when nothing matches; nested outlets render nothing |
| error | Shown when a lazy import rejects ({ error?: unknown } props) |
Put <RouterOutlet depth={1} /> inside a parent layout to render the child.
Hooks
All hooks require provide(RouterKey, router) on an ancestor.
import {
useRouter,
useParams,
useSearchParams,
useRoute,
useNavigate,
usePath,
} from "sinwan-router";
const params = useParams(); // { id: "42" }
const search = useSearchParams(); // URLSearchParams
const path = usePath(); // "/users/42"
const route = useRoute(); // MatchedRoute | null
const navigate = useNavigate();
navigate("/about", { replace: true });Features
- Radix-tree matcher:
createRadixTree,radixInsert,radixSearch,buildPath,normalizePath - Router:
createRouter,navigate,prefetch,resolveComponent,dispose,lazy/isLazyComponent - Components:
Link,NavLink,RouterOutlet,Routes,Route - Hooks:
useRouter,useParams,useSearchParams,useRoute,useNavigate,usePath,RouterKey
Documentation
Development
bun test
bun run typecheck
bun run buildAuthor
Mohammed Ben Cheikh
License
MIT
