Installation and setup
npm i reactive-routeyarn add reactive-routepnpm add reactive-routeReactive Route is an npm package without any dependencies, with separate module imports for seamless integration with an existing reactivity system on any framework (React, Preact, Solid, Vue). There is no need to configure tree-shaking.
Peer dependencies
If the framework's built-in reactivity is not used, the appropriate reactive libraries must be installed (see Integrations).
Module map
import { createConfigs, createRouter } from 'reactive-route';
import { Router } from 'reactive-route/solid';
import { Router } from 'reactive-route/react';
import { Router } from 'reactive-route/preact';
import { Router } from 'reactive-route/vue';
import { adapters } from 'reactive-route/adapters/mobx-react';
import { adapters } from 'reactive-route/adapters/mobx-preact';
import { adapters } from 'reactive-route/adapters/mobx-solid';
import { adapters } from 'reactive-route/adapters/solid';
import { adapters } from 'reactive-route/adapters/kr-observable-react';
import { adapters } from 'reactive-route/adapters/kr-observable-preact';
import { adapters } from 'reactive-route/adapters/kr-observable-solid';
import { adapters } from 'reactive-route/adapters/vue'Creating configuration
In Reactive Route terminology, the description of a route and its behavior is called Config and is passed to createConfigs:
import { createConfigs, createRouter } from 'reactive-route';
import { adapters } from 'reactive-route/adapters/{reactive-system}';
const configs = createConfigs({
home: {
path: '/',
loader: () => import('./pages/home'),
}
});
export const router = createRouter({ configs, adapters });
// somewhere
await router.redirect({ name: 'home' });loader expects the page component to be in the default export.
If necessary, you can declare path with variables like /:id/:name; in that case, each variable requires a validator. Never trust data coming from the URL, especially if SSR is involved.
Not recommended
to pass the component directly as loader: () => Promise.resolve({ default: HomePage }) to avoid cyclic imports
Passing configuration as an object is more type-safe
"vue-router": "5.0.0"
const routes = [
{ name: 'home', path: '/', component: Home }
]
"@kitbag/router": "0.22.0"
const routes = [
createRoute({ name: 'home', path: '/', component: Home })
]In routing libraries, configuration is often passed as an array, which creates the possibility of name collisions, and TypeScript cannot guarantee their uniqueness.
Reactive Route automatically adds name to Config, equal to the object key, thus providing unique keys, named routes, and the absence of collisions even before the code runs.
The library also minimizes boilerplate — there is no need to call a separate function for each Config:
"mobx-router": "1.0.0"
const configs = {
home: new Route({ path: '/', component: <Home /> })
}
"@tanstack/react-router": "1.157.15"
const rootRoute = createRootRoute()
const indexRoute = createRoute({
getParentRoute: () => rootRoute,
path: '/',
component: Home,
})
const routeTree = rootRoute.addChildren([indexRoute])Using async import expands capabilities
Async imports are supported by all modern bundlers, allowing code to be split into chunks (code-splitting).
It is also a powerful tool for modular architectures, where an import returns not only default: Component, but also page-level stores / APIs / other module services, and Reactive Route allows you to work with them.
Chunk files are loaded in an optimized way — only after all checks and lifecycle steps have passed, and if necessary they can be preloaded manually via router.preloadComponent.
It also eliminates the possibility of cyclic imports that can break the application:
"mobx-router": "1.0.0"
import { Home } from './Home';
export const routes = {
home: new Route({ path: '/', component: <Home /> })
}"mobx-router": "1.0.0"
import { routes } from './routes';
export function Home() {
const store = useContext(StoreContext);
return <Link router={store.router} route={routes.home} />
});Export
It is recommended to use the Context API to pass the router into components.
But you can choose the approach yourself
Exporting via the Singleton pattern (as in the previous example) is simpler and is suitable for CSR projects.
In SSR, however, several users can open different pages at the same time, and a Singleton will render HTML markup from the latest current state rather than the one needed for a specific user, so isolation via contexts or another form of DI is required.
import { createContext, useContext } from 'react';
import { createConfigs, createRouter } from 'reactive-route';
import { adapters } from 'reactive-route/adapters/{reactive-system}';
export function getRouter() {
const configs = createConfigs({
home: {
path: '/',
loader: () => import('./pages/home'),
},
user: {
path: '/user/:id',
params: {
id: (value) => /^\d+$/.test(value)
},
query: {
phone: (value) => value.length < 15
},
loader: () => import('./pages/user'),
},
notFound: {
path: '/not-found',
props: { errorCode: 404 },
loader: () => import('./pages/error'),
},
internalError: {
path: '/internal-error',
props: { errorCode: 500 },
loader: () => import('./pages/error'),
}
});
return createRouter({ adapters, configs });
}
export const RouterContext = createContext<{
router: ReturnType<typeof getRouter>
}>(undefined);
export function useRouter() {
return useContext(RouterContext);
}import { createContext } from 'preact';
import { useContext } from 'preact/hooks';
import { createConfigs, createRouter } from 'reactive-route';
import { adapters } from 'reactive-route/adapters/{reactive-system}';
export function getRouter() {
const configs = createConfigs({
home: {
path: '/',
loader: () => import('./pages/home'),
},
user: {
path: '/user/:id',
params: {
id: (value) => /^\d+$/.test(value)
},
query: {
phone: (value) => value.length < 15
},
loader: () => import('./pages/user'),
},
notFound: {
path: '/not-found',
props: { errorCode: 404 },
loader: () => import('./pages/error'),
},
internalError: {
path: '/internal-error',
props: { errorCode: 500 },
loader: () => import('./pages/error'),
}
});
return createRouter({ adapters, configs });
}
export const RouterContext = createContext<{
router: ReturnType<typeof getRouter>
}>(undefined);
export function useRouter() {
return useContext(RouterContext);
}import { createContext, useContext } from 'solid-js';
import { createConfigs, createRouter } from 'reactive-route';
import { adapters } from 'reactive-route/adapters/{reactive-system}';
export function getRouter() {
const configs = createConfigs({
home: {
path: '/',
loader: () => import('./pages/home'),
},
user: {
path: '/user/:id',
params: {
id: (value) => /^\d+$/.test(value)
},
query: {
phone: (value) => value.length < 15
},
loader: () => import('./pages/user'),
},
notFound: {
path: '/not-found',
props: { errorCode: 404 },
loader: () => import('./pages/error'),
},
internalError: {
path: '/internal-error',
props: { errorCode: 500 },
loader: () => import('./pages/error'),
}
});
return createRouter({ adapters, configs });
}
export const RouterContext = createContext<{
router: ReturnType<typeof getRouter>
}>(undefined);
export function useRouter() {
return useContext(RouterContext);
}import { InjectionKey, inject } from 'vue';
import { createConfigs, createRouter } from 'reactive-route';
import { adapters } from 'reactive-route/adapters/{reactive-system}';
export function getRouter() {
const configs = createConfigs({
home: {
path: '/',
loader: () => import('./pages/home'),
},
user: {
path: '/user/:id',
params: {
id: (value) => /^\d+$/.test(value)
},
query: {
phone: (value) => value.length < 15
},
loader: () => import('./pages/user'),
},
notFound: {
path: '/not-found',
props: { errorCode: 404 },
loader: () => import('./pages/error'),
},
internalError: {
path: '/internal-error',
props: { errorCode: 500 },
loader: () => import('./pages/error'),
}
});
return createRouter({ adapters, configs });
}
export const routerStoreKey: InjectionKey<{
router: ReturnType<typeof getRouter>
}> = Symbol();
export function useRouter() {
return inject(routerStoreKey)!;
}`notFound` and `internalError` are required
"vue-router": "5.0.0"
const routes = [
{ path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFound }
]
"mobx-router": "1.0.0"
startRouter(
configs,
store,
{ notfound: () => store.router.goTo(YOUR_NOT_FOUND_ROUTE), }
);
"@kitbag/router": "0.22.0"
export const router = createRouter(routes, {
rejections: [
createRejection({ type: 'NotFound', component: NotFoundPage })
]
})In routing libraries, the syntax for defining error pages often differs greatly from "regular pages", and their presence is optional, which in some cases leads to rendering a blank page.
In Reactive Route, they are required to ensure stability, but you are allowed to define only path, loader, and props. You can navigate to them normally with router.redirect({ name: 'notFound' }).
internalError will be rendered without replacing the URL in browser history on unhandled lifecycle exceptions or a page chunk loading error. Reactive Route acts here as the "last line of defense" and provides additional consistency for the application.
Launch
The router is ready to work; all that remains is to find the initial Config that matches the browser URL and render the page component.
import { createRoot } from 'react-dom/client';
import { App } from './App';
import { getRouter, RouterContext } from './router';
const router = getRouter();
await router.init(location.href);
createRoot(document.getElementById('app')!).render(
<RouterContext.Provider value={{ router }}>
<App />
</RouterContext.Provider>
);import { Router } from 'reactive-route/react';
import { useRouter } from './router';
export function App() {
const { router } = useRouter();
return <Router router={router} />;
}import { render } from 'preact';
import { App } from './App';
import { getRouter, RouterContext } from './router';
const router = getRouter();
await router.init(location.href);
render(
<RouterContext.Provider value={{ router }}>
<App />
</RouterContext.Provider>,
document.getElementById('app')!
);import { Router } from 'reactive-route/preact';
import { useRouter } from './router';
export function App() {
const { router } = useRouter();
return <Router router={router} />;
}import { render } from 'solid-js/web';
import { App } from './App';
import { getRouter, RouterContext } from './router';
const router = getRouter();
await router.init(location.href);
render(
() => (
<RouterContext.Provider value={{ router }}>
<App />
</RouterContext.Provider>
),
document.getElementById('app')!
);import { Router } from 'reactive-route/solid';
import { useRouter } from './router';
export function App() {
const { router } = useRouter();
return <Router router={router} />;
}import { createApp } from 'vue';
import App from './App.vue';
import { getRouter, routerStoreKey } from './router';
const router = getRouter();
await router.init(location.href);
createApp(App, { router })
.provide(routerStoreKey, { router })
.mount('#app');<script lang="ts" setup>
import { Router } from 'reactive-route/vue';
import { useRouter } from './router';
const { router } = useRouter();
</script>
<template>
<Router :router="router" />
</template>