How it works
Reactive Route has strong typing, well-thought-out redirect logic, and fault tolerance that are extremely hard to "break". For most scenarios, it is enough to define the configurations and use router.redirect; TS will guide you through the rest.
When working with the router, there are only two main structures:
Config
Contains all page logic
user: {
path: '/user/:id',
params: {
id: (value) => /^\d+$/.test(value)
},
query: {
phone: (value) => value.length < 15
},
loader: () => import('./pages/user'),
props: { foo: 'bar' },
async beforeEnter({ nextState, redirect }) {
await api.loadUser();
if (!store.isLoggedIn) {
return redirect({
name: 'login',
query: { returnTo: nextState.name }
});
}
},
async beforeLeave({ nextState, preventRedirect }) {
if (nextState.name === 'home') {
return preventRedirect();
}
}
}Config typing tests
import { createConfigs } from '../../packages/core';
const loader = async () => ({ default: null });
const v = () => true;
createConfigs({
dynamic: { path: '/:id/:tab', params: { id: v, tab: v }, loader },
static: { path: '/', loader },
notFound: { path: '/404', props: {}, loader },
internalError: { path: '/500', props: {}, loader },
// @ts-expect-error "params" must be "never"
static1: { path: '/', loader, params: {} },
// @ts-expect-error "params" are required
dynamic1: { path: '/:id/:tab', loader },
// @ts-expect-error "params.tab" and "params.id" are required
dynamic2: { path: '/:id/:tab', params: {}, loader },
// @ts-expect-error "params.tab" is required
dynamic3: { path: '/:id/:tab', params: { id: v }, loader },
// @ts-expect-error "params" validators must be functions
dynamic4: { path: '/:id/:tab', params: { id: v, tab: '' }, loader },
// @ts-expect-error duplicates are not allowed
double1: { path: '/:id/:id', params: { id: v }, loader },
// @ts-expect-error duplicates are not allowed
double2: { path: '/:id/:id', loader },
// @ts-expect-error duplicates are not allowed
double3: { path: '/:id/:id/', params: { id: v }, loader },
// @ts-expect-error duplicates are not allowed
double4: { path: '/:id/:id/', loader },
// @ts-expect-error duplicates are not allowed
double5: { path: '/:id/:tab/:id', params: { id: v, tab: v }, loader },
// @ts-expect-error duplicates are not allowed
double6: { path: '/:id/:tab/:id', loader },
});
// @ts-expect-error internalError is required
createConfigs({ notFound: { path: '/404', loader } });
// @ts-expect-error notFound is required
createConfigs({ internalError: { path: '/500', loader } });
createConfigs({
// @ts-expect-error "query" is not allowed
notFound: { path: '/404', loader, query: {} },
// @ts-expect-error "params" are not allowed
internalError: { path: '/500', loader, params: { code: v } },
});
createConfigs({
// @ts-expect-error "beforeLeave" is not allowed
notFound: { path: '/404', loader, beforeLeave: () => undefined },
// @ts-expect-error "beforeEnter" is not allowed
internalError: { path: '/500', loader, beforeEnter: () => undefined },
});State
Contains the current values of a specific Config
// pages/user/index.tsx
import { useRouter } from 'router';
export default function PageUser() {
const { router } = useRouter();
const pageState = router.state.user!;
console.log(pageState);
return (
<>
ID: {pageState.params.id}
Phone: {pageState.query.phone}
</>
)
}
// console output (Observable object)
{
name: 'user',
params: { id: '9999' },
query: { phone: '123456' }
}// pages/user/index.tsx
import { useRouter } from 'router';
export default function PageUser() {
const { router } = useRouter();
const pageState = router.state.user!;
console.log(pageState);
return (
<>
ID: {pageState.params.id}
Phone: {pageState.query.phone}
</>
)
}
// console output (Observable object)
{
name: 'user',
params: { id: '9999' },
query: { phone: '123456' }
}// pages/user/index.tsx
import { useRouter } from 'router';
export default function PageUser() {
const { router } = useRouter();
console.log(router.state.user);
return (
<>
ID: {router.state.user!.params.id}
Phone: {router.state.user!.query.phone}
</>
)
}
// console output (Observable object)
{
name: 'user',
params: { id: '9999' },
query: { phone: '123456' }
}// pages/user/User.vue
<script lang="ts" setup>
import { useRouter } from 'router';
const { router } = useRouter();
const pageState = router.state.user!;
console.log(pageState);
</script>
<template>
ID: {pageState.params.id}
Phone: {pageState.query.phone}
</template>
<script lang="ts">
// console output (Observable object)
{
name: 'user',
params: { id: '9999' },
query: { phone: '123456' }
}
</script>INFO
The "non-null assertion" operator is safe if only one Config uses this page component.
State typing tests
import { createConfigs, createRouter } from '../../packages/core';
const loader = async () => ({ default: null });
const v: (value: string) => boolean = () => true;
const adapters = {} as any;
const configs = createConfigs({
static: { path: '/', loader },
staticQuery: { path: '/', query: { q: v }, loader },
dynamic: { path: '/:id', params: { id: v }, loader },
dynamicQuery: {
path: '/:id',
params: { id: v },
query: { q: v },
loader,
},
notFound: { path: '/404', loader },
internalError: { path: '/500', loader },
});
const router = createRouter({ configs, adapters });
// @ts-expect-error not existing name
router.state.unknown;
router.state.static!.name;
router.state.static!.params;
router.state.static!.query;
// @ts-expect-error "params" values are not available
router.state.static!.params.unknown;
// @ts-expect-error "query" values are not available
router.state.static!.query.unknown;
router.state.notFound!.name;
router.state.notFound!.params;
// @ts-expect-error "params" values are not available
router.state.notFound!.params.unknown;
router.state.notFound!.query;
// @ts-expect-error "query" values are not available
router.state.notFound!.query.unknown;
router.state.internalError!.name;
router.state.internalError!.params;
// @ts-expect-error "params" values are not available
router.state.internalError!.params.unknown;
router.state.internalError!.query;
// @ts-expect-error "query" values are not available
router.state.internalError!.query.unknown;
router.state.staticQuery!.name;
router.state.staticQuery!.params;
// @ts-expect-error "params" values are not available
router.state.staticQuery!.params.unknown;
router.state.staticQuery!.query.q;
// @ts-expect-error unknown "query" are not available
router.state.staticQuery!.query.unknown;
router.state.dynamic!.name;
router.state.dynamic!.params.id;
// @ts-expect-error unknown "params" are not available
router.state.dynamic!.params.unknown;
router.state.dynamic!.query;
// @ts-expect-error "query" values are not available
router.state.dynamic!.query.unknown;
router.state.dynamicQuery!.name;
router.state.dynamicQuery!.params.id;
router.state.dynamicQuery!.query.q;
// @ts-expect-error unknown "params" are not available
router.state.dynamicQuery!.params.unknown;
// @ts-expect-error unknown "query" are not available
router.state.dynamicQuery!.query.unknown;Type Narrowing for State
import { createConfigs, createRouter } from '../../packages/core';
const loader = async () => ({ default: null });
const v: (value: string) => boolean = () => true;
const adapters = {} as any;
const configs = createConfigs({
static: { path: '/', loader },
staticQuery: { path: '/', query: { q: v }, loader },
dynamic: { path: '/:id', params: { id: v }, loader },
dynamicQuery: {
path: '/:id',
params: { id: v },
query: { q: v },
loader,
},
notFound: { path: '/404', loader },
internalError: { path: '/500', loader },
});
const router = createRouter({ configs, adapters });
const state = router.urlToState('');
// @ts-expect-error not existing name
state.name === 'unknown';
if (state.name === 'notFound') {
state.name;
state.params;
// @ts-expect-error "params" values are not available
state.params.unknown;
state.query;
// @ts-expect-error "query" values are not available
state.query.unknown;
}
if (state.name === 'internalError') {
state.name;
state.params;
// @ts-expect-error "params" values are not available
state.params.unknown;
state.query;
// @ts-expect-error "query" values are not available
state.query.unknown;
}
if (state.name === 'static') {
state.name;
state.params;
state.query;
// @ts-expect-error "params" values are not available
state.params.unknown;
// @ts-expect-error "query" values are not available
state.query.unknown;
}
if (state.name === 'staticQuery') {
state.name;
state.params;
// @ts-expect-error "params" values are not available
state.params.unknown;
state.query.q;
// @ts-expect-error unknown "query" are not available
state.query.unknown;
}
if (state.name === 'dynamic') {
state.name;
state.params.id;
// @ts-expect-error unknown "params" are not available
state.params.unknown;
state.query;
// @ts-expect-error "query" values are not available
state.query.unknown;
}
if (state.name === 'dynamicQuery') {
state.name;
state.params.id;
state.query.q;
// @ts-expect-error unknown "params" are not available
state.params.unknown;
// @ts-expect-error unknown "query" are not available
state.query.unknown;
}This is a normalized structure in which params and query objects are always present. However, if they are not described in the corresponding configuration, their type will be Record<never, string>. This is done to protect runtime behavior, because a developer might add ts-ignore somewhere and, without default values, the code would crash with Cannot read property of undefined.
However, for the redirect mechanism it is more convenient to work with more strict types, and for this purpose StateDynamic was created, which completely forbids passing params and query if they were not present in the page configuration.
StateDynamic typing tests
import { createConfigs, createRouter } from '../../packages/core';
const loader = async () => ({ default: null });
const v: (value: string) => boolean = () => true;
const adapters = {} as any;
const configs = createConfigs({
static: { path: '/', loader },
staticQuery: { path: '/', query: { q: v }, loader },
dynamic: { path: '/:id', params: { id: v }, loader },
dynamicQuery: {
path: '/:id',
params: { id: v },
query: { q: v },
loader,
},
notFound: { path: '/404', loader },
internalError: { path: '/500', loader },
});
const router = createRouter({ configs, adapters });
await router.redirect({ name: 'static' });
await router.redirect({ name: 'staticQuery' });
await router.redirect({ name: 'staticQuery', query: { q: '' } });
await router.redirect({ name: 'dynamic', params: { id: '' } });
await router.redirect({ name: 'dynamicQuery', params: { id: '' } });
await router.redirect({
name: 'dynamicQuery',
params: { id: '' },
query: { q: '' },
});
/** Not string values raise errors */
await router.redirect({
// @ts-expect-error name must be a string
name: 1,
});
await router.redirect({
name: 'staticQuery',
// @ts-expect-error "query" values must be strings
query: { q: 1 },
});
await router.redirect({
name: 'dynamic',
// @ts-expect-error "params" values must be strings
params: { id: 1 },
});
await router.redirect({
name: 'dynamicQuery',
params: { id: '' },
// @ts-expect-error "query" values must be strings
query: { q: 1 },
});
// @ts-expect-error "params" are required
await router.redirect({ name: 'dynamic' });
await router.redirect({
name: 'dynamic',
// @ts-expect-error "params" are incomplete
params: {},
});
/** Irrelevant StateDynamic raise errors */
// @ts-expect-error name must be present
await router.redirect({});
// @ts-expect-error "name" must be a string key of configs
await router.redirect({ name: 'unknown' });
await router.redirect({
name: 'static',
// @ts-expect-error "params" are not allowed
params: { id: '' },
});
await router.redirect({
name: 'staticQuery',
// @ts-expect-error "params" are not allowed
params: { id: '' },
});
await router.redirect({
name: 'dynamic',
// @ts-expect-error extra "params" are not allowed
params: { id: '', extra: '' },
});
await router.redirect({
name: 'static',
// @ts-expect-error "query" is not allowed
query: { q: '' },
});
await router.redirect({
name: 'dynamic',
params: { id: '' },
// @ts-expect-error "query" is not allowed
query: { q: '' },
});
await router.redirect({
name: 'staticQuery',
// @ts-expect-error unknown "query" key
query: { unknown: '' },
});
await router.redirect({
name: 'dynamicQuery',
params: { id: '' },
// @ts-expect-error unknown "query" key
query: { unknown: '' },
});
const state = router.urlToState('');
void router.redirect(state);Decoding
The browser works with URLs in an encoded format, so Reactive Route has built-in encoding and decoding mechanisms. Let us look at the initialization process of the first redirect (if all validators are replaced with () => true):
await router.init(`/user/with%20space?phone=and%26symbols`);The router does not understand the string format, because it is absent from the structures described above.
For conversion, there is the router.urlToState method, which cleans the URL of unnecessary parts, decodes values, runs validators, and returns a State that the router understands, and the redirect is then called with it:
await router.redirect({
name: 'user',
params: { id: 'with space' },
query: { phone: 'and&symbols' }
})Important
Validators receive decoded values for convenience when working with non-English URLs:
id: (value) => console.log(value) will print not with%20space, but with space
phone: (value) => console.log(value) will print not and%26symbols, but and&symbols
Redirect flow
- the redirect reason is investigated. In this case,
reason = 'new_config' beforeLeaveof the previousConfigis executed (in this case there was none, so it is skipped)beforeEnterof the nextConfigis executed, including any redirect chain- the js chunk is loaded (if code-splitting is enabled) with the component and other exports
- the normalized
Stateis written to the correspondingrouter.state[config.name], in this caserouter.state.user - if synchronization with the History API is enabled (by default it is enabled for the browser environment), native
pushState / replaceStateare called