Skip to content

API

Config

Contains all page logic

Configurable properties
ts
path: string
Route path, must start with / and may include dynamic segments
ts
props?: Record<string, any>
Static props passed to the page component
ts
params?: Record<
  TypeExtractParams<TPath>,
  (value: string) => boolean
>
Validators for dynamic path segments
ts
query?: Record<
  string,
  (value: string) => boolean
>
Validators for `query` parameters
ts
loader: () => Promise<{
  default: PageComponent,
  ...otherExports
}>
Function that returns a Promise with the component in the default property
ts
beforeEnter?: (data: {
  reason: TypeReason;
  nextState: TypeStateUntyped;
  currentState?: TypeStateUntyped;
  redirect: (
    stateDynamic: 
      TypeStateDynamicUntyped & 
      { replace?: boolean; }
  ) => void;
}) => Promise<void | (
  TypeStateDynamicUntyped & 
  { replace?: boolean; }
)>;
Lifecycle function called before redirecting to the page
ts
beforeLeave?: (data: {
  reason: TypeReason;
  nextState: TypeStateUntyped;
  currentState: TypeStateUntyped;
  preventRedirect: () => void;
}) => Promise<void>;
Lifecycle function called before leaving the page
System properties
ts
name: string

Corresponds to the object key

ts
component?: any
The default field returned by loader
ts
otherExports?: Record<string, any>

All exports returned by loader except default

config.params

In Reactive Route, there is no explicit split between static and dynamic Config; path segments prefixed with : are controlled by validators that allow a page to open with a value from the URL, and this is reflected in the typings.

ts
users: {
  path: '/users',  // no validators
  loader: () => import('./pages/users')
},
userDetails: {
  path: '/user/:id',  // "id" is a param matched by the validator
  params: {
    id: (value) => /^\d+$/.test(value)
  },
  loader: () => import('./pages/userDetails')
}
Config typing tests
ts
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 },
});

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

This way, Reactive Route guarantees that all parameters in router.state.userDetails.params have passed validation and can be used safely.

At runtime, path is always the source of truth, and stable matching is guaranteed even with TS disabled. The declaration order of Config does not matter — the router algorithm has only one rule: when the URL fully matches path, that config has the highest priority, even if it is declared last. In all other scenarios, the first Config whose validators all pass wins.

config.query

They are described in the same format as params, as validators:

ts
search: {
  path: '/search',
  query: {
    userPrompt: (value) => value.length > 5
  },
  loader: () => import('./pages/search')
}

Important

Validators receive decoded values:

userPrompt: (value) => console.log(value) will print not with%20space, but with space

All query parameters are optional, and their absence does not cause a redirect to notFound.

ts
const pageState = router.state.search;

await router.redirect({ name: 'search', query: { userPrompt: 'short' }})

console.log(pageState.query.userPrompt) // undefined

await router.redirect({ name: 'search', query: { userPrompt: 'enough' }})

console.log(pageState.query.userPrompt) // 'enough'

If you need certain query parameters to be present, you can check them in beforeEnter and redirect to notFound if they are missing from nextState.query.

config.beforeEnter

This async function can be used to redirect to another Config, perform auth checks, and load data. Unhandled errors will lead to rendering internalError without changing the URL in the browser.

Arguments
ts
reason: 
  | 'new_query' 
  | 'new_params' 
  | 'new_config'
Reason why beforeEnter was called. If both params and query changed, then new_params
ts
nextState: TypeStateUntyped
Next expected State
ts
currentState?: TypeStateUntyped
Current active State (undefined on the very first redirect)
ts
redirect: (
  stateDynamic: 
    TypeStateDynamicUntyped &
    { replace?: boolean; }
) => void
Method for redirecting inside the lifecycle. Since createConfigs is called before the router is created, router.redirect cannot be used here
ts
dashboard: {
  path: '/dashboard',
  loader: () => import('./pages/dashboard'),
  async beforeEnter({ redirect }) {
    await api.loadUser();

    if (!store.isAuthenticated()) {
      return redirect({ name: 'login', query: { returnTo: 'dashboard' } });
    }

    await api.loadDashboard();
  }
}
beforeEnter / beforeLeave typing tests
ts
import { createConfigs } from '../../packages/core';

const loader = async () => ({ default: null });

createConfigs({
  static: {
    path: '/',
    loader,
    async beforeEnter({ currentState, nextState, reason, redirect }) {
      currentState?.name;
      currentState?.params;
      currentState?.query;
      // @ts-expect-error unknown "currentState" property is not available
      currentState?.unknown;

      nextState.name;
      nextState.params;
      nextState.query;
      // @ts-expect-error unknown "nextState" property is not available
      nextState.unknown;

      reason satisfies 'unmodified' | 'new_query' | 'new_params' | 'new_config';
      // @ts-expect-error unknown reason is not assignable
      reason satisfies 'unknown';

      redirect({ name: 'static' });
      redirect({ name: 'static', replace: true });
      redirect({ name: 'unknown' });
      // @ts-expect-error "replace" must be boolean
      redirect({ name: 'static', replace: 'true' });
      // @ts-expect-error redirect params values must be strings
      redirect({ name: 'dynamic', params: { id: 1 } });
    },
    async beforeLeave({ currentState, nextState, reason, preventRedirect }) {
      currentState.name;
      currentState.params;
      currentState.query;
      // @ts-expect-error unknown "currentState" property is not available
      currentState.unknown;

      nextState.name;
      nextState.params;
      nextState.query;
      // @ts-expect-error unknown "nextState" property is not available
      nextState.unknown;

      reason satisfies 'unmodified' | 'new_query' | 'new_params' | 'new_config';
      // @ts-expect-error unknown reason is not assignable
      reason satisfies 'unknown';

      preventRedirect();
      // @ts-expect-error "preventRedirect" does not accept arguments
      preventRedirect(true);
    },
  },
  notFound: { path: '/404', loader },
  internalError: { path: '/500', loader },
});

Limitations

Only in lifecycle functions do redirect, currentState, and nextState have incomplete typings (name is just string) due to TypeScript 5 limitations, so TS will not report errors during refactoring.

TIP

Always use return with redirect and preventRedirect for stable redirect logic.

config.beforeLeave

This async function can be used to interrupt a redirect. Unhandled errors will lead to rendering internalError without changing the URL in the browser.

Arguments
ts
reason: 
  | 'new_query' 
  | 'new_params' 
  | 'new_config'
Reason why beforeLeave was called. If both params and query changed, then new_params
ts
nextState: TypeStateUntyped
Next expected State
ts
currentState: TypeStateUntyped
Current active State
ts
preventRedirect: () => void
Method for stopping the redirect
ts
dashboard: {
  path: '/dashboard',
  loader: () => import('./pages/dashboard'),
  async beforeLeave({ preventRedirect, nextState }) {
    if (nextState.name === 'login') return preventRedirect();

    // Do not check for unsaved changes on the server
    if (typeof window === 'undefined') return;

    const hasUnsavedChanges = await api.checkForm();

    if (hasUnsavedChanges && !window.confirm(
      `You have unsaved changes. Are you sure you want to leave?`
    )) return preventRedirect();
  }
}
beforeEnter / beforeLeave typing tests
ts
import { createConfigs } from '../../packages/core';

const loader = async () => ({ default: null });

createConfigs({
  static: {
    path: '/',
    loader,
    async beforeEnter({ currentState, nextState, reason, redirect }) {
      currentState?.name;
      currentState?.params;
      currentState?.query;
      // @ts-expect-error unknown "currentState" property is not available
      currentState?.unknown;

      nextState.name;
      nextState.params;
      nextState.query;
      // @ts-expect-error unknown "nextState" property is not available
      nextState.unknown;

      reason satisfies 'unmodified' | 'new_query' | 'new_params' | 'new_config';
      // @ts-expect-error unknown reason is not assignable
      reason satisfies 'unknown';

      redirect({ name: 'static' });
      redirect({ name: 'static', replace: true });
      redirect({ name: 'unknown' });
      // @ts-expect-error "replace" must be boolean
      redirect({ name: 'static', replace: 'true' });
      // @ts-expect-error redirect params values must be strings
      redirect({ name: 'dynamic', params: { id: 1 } });
    },
    async beforeLeave({ currentState, nextState, reason, preventRedirect }) {
      currentState.name;
      currentState.params;
      currentState.query;
      // @ts-expect-error unknown "currentState" property is not available
      currentState.unknown;

      nextState.name;
      nextState.params;
      nextState.query;
      // @ts-expect-error unknown "nextState" property is not available
      nextState.unknown;

      reason satisfies 'unmodified' | 'new_query' | 'new_params' | 'new_config';
      // @ts-expect-error unknown reason is not assignable
      reason satisfies 'unknown';

      preventRedirect();
      // @ts-expect-error "preventRedirect" does not accept arguments
      preventRedirect(true);
    },
  },
  notFound: { path: '/404', loader },
  internalError: { path: '/500', loader },
});

Limitations

Only in lifecycle functions do redirect, currentState, and nextState have incomplete typings (name is just string) due to TypeScript 5 limitations, so TS will not report errors during refactoring.

TIP

Always use return with redirect and preventRedirect for stable redirect logic.

State

A reactive object stored in router.state.

Properties
ts
name: string
Corresponds to the Config key
ts
params: Record<
  keyof TConfig['params'], 
  string
>
Validated and decoded `params` values. All of them will always be present
ts
query: Partial<Record<
  keyof TConfig['query'], 
  string
>>
Validated and decoded `query` values. All are optional
State typing tests
ts
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
ts
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;
}

Router

createRouter

This function creates router.

Arguments
ts
configs: ReturnType<typeof createConfigs>
Object with Configs
ts
adapters: TypeAdapters
Adapters for the reactivity system
ts
beforeComponentChange?: (params: {
  prevState?: TypeState;
  prevConfig?: TypeConfig;

  currentState: TypeState;
  currentConfig: TypeConfig;
}) => void
Global lifecycle function that runs only when the component changes (not the page!)

beforeComponentChange

This function is called only when the rendered component changes and is intended for use in modular architectures.

ts
// Some page exports "export class PageStore { data: {}, destroy() {} }"

const globalStore = { pages: {} };

createRouter({
  configs,
  adapters,
  beforeComponentChange({ prevConfig, currentConfig }) {
    // PageStore is accessible from the otherExports object
    const { PageStore } = currentConfig.otherExports;

    if (PageStore) globalStore.pages[currentConfig.name] = new PageStore();

    // destroy the obsolete PageStore from the previuos page
    globalStore.pages[prevConfig.name]?.destroy();
  }
})
beforeComponentChange typing tests
ts
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 },
});

createRouter({
  configs,
  adapters,
  beforeComponentChange({ prevConfig, prevState, currentConfig, currentState }) {
    /** prevConfig */

    prevConfig?.name;
    prevConfig?.loader;
    // @ts-expect-error unknown prevConfig property is not available
    prevConfig?.unknown;

    if (prevConfig?.name === 'dynamic') {
      prevConfig.params.id;
      // @ts-expect-error unknown "params" are not available
      prevConfig.params.unknown;
      // @ts-expect-error "query" is not available
      prevConfig.query.q;
    }

    if (prevConfig?.name === 'static') {
      // @ts-expect-error "params" is not available
      prevConfig.params.id;
      // @ts-expect-error "query" is not available
      prevConfig.query.q;
    }

    if (prevConfig?.name === 'staticQuery') {
      prevConfig.query.q;
      // @ts-expect-error unknown "query" are not available
      prevConfig.query.unknown;
      // @ts-expect-error "params" is not available
      prevConfig.params.id;
    }

    if (prevConfig?.name === 'dynamicQuery') {
      prevConfig.params.id;
      prevConfig.query.q;
      // @ts-expect-error unknown "params" are not available
      prevConfig.params.unknown;
      // @ts-expect-error unknown "query" are not available
      prevConfig.query.unknown;
    }

    if (prevConfig?.name === 'notFound') {
      // @ts-expect-error "params" is not available
      prevConfig.params.code;
      // @ts-expect-error "query" is not available
      prevConfig.query.q;
    }

    if (prevConfig?.name === 'internalError') {
      // @ts-expect-error "params" is not available
      prevConfig.params.code;
      // @ts-expect-error "query" is not available
      prevConfig.query.q;
    }

    /** currentConfig */

    currentConfig.name;
    currentConfig.loader;
    // @ts-expect-error unknown currentConfig property is not available
    currentConfig.unknown;

    if (currentConfig.name === 'notFound') {
      // @ts-expect-error "params" is not available
      currentConfig.params.code;
      // @ts-expect-error "query" is not available
      currentConfig.query.q;
    }

    if (currentConfig.name === 'internalError') {
      // @ts-expect-error "params" is not available
      currentConfig.params.code;
      // @ts-expect-error "query" is not available
      currentConfig.query.q;
    }

    if (currentConfig.name === 'static') {
      // @ts-expect-error "params" is not available
      currentConfig.params.id;
      // @ts-expect-error "query" is not available
      currentConfig.query.q;
    }

    if (currentConfig.name === 'staticQuery') {
      currentConfig.query.q;
      // @ts-expect-error unknown "query" are not available
      currentConfig.query.unknown;
      // @ts-expect-error "params" is not available
      currentConfig.params.id;
    }

    if (currentConfig.name === 'dynamicQuery') {
      currentConfig.params.id;
      currentConfig.query.q;
      // @ts-expect-error unknown "params" are not available
      currentConfig.params.unknown;
      // @ts-expect-error unknown "query" are not available
      currentConfig.query.unknown;
    }

    if (currentConfig.name === 'dynamic') {
      currentConfig.params.id;
      // @ts-expect-error unknown "params" are not available
      currentConfig.params.unknown;
      // @ts-expect-error "query" is not available
      currentConfig.query.q;
    }

    /** prevState */

    prevState?.name;
    prevState?.params;
    prevState?.query;
    // @ts-expect-error unknown prevState property is not available
    prevState?.unknown;

    // @ts-expect-error not existing name
    prevState?.name === 'unknown';

    if (prevState?.name === 'staticQuery') {
      prevState.query.q;
      // @ts-expect-error unknown "query" are not available
      prevState.query.unknown;
      // @ts-expect-error "params" values are not available
      prevState.params.unknown;
    }

    if (prevState?.name === 'notFound') {
      prevState.params;
      // @ts-expect-error "params" values are not available
      prevState.params.unknown;
      prevState.query;
      // @ts-expect-error "query" values are not available
      prevState.query.unknown;
    }

    if (prevState?.name === 'internalError') {
      prevState.params;
      // @ts-expect-error "params" values are not available
      prevState.params.unknown;
      prevState.query;
      // @ts-expect-error "query" values are not available
      prevState.query.unknown;
    }

    if (prevState?.name === 'static') {
      prevState.params;
      prevState.query;
      // @ts-expect-error "params" values are not available
      prevState.params.unknown;
      // @ts-expect-error "query" values are not available
      prevState.query.unknown;
    }

    if (prevState?.name === 'dynamic') {
      prevState.params.id;
      // @ts-expect-error unknown "params" are not available
      prevState.params.unknown;
      prevState.query;
      // @ts-expect-error "query" values are not available
      prevState.query.unknown;
    }

    if (prevState?.name === 'dynamicQuery') {
      prevState.params.id;
      prevState.query.q;
      // @ts-expect-error unknown "params" are not available
      prevState.params.unknown;
      // @ts-expect-error unknown "query" are not available
      prevState.query.unknown;
    }

    /** currentState */

    currentState.name;
    currentState.params;
    currentState.query;
    // @ts-expect-error unknown currentState property is not available
    currentState.unknown;

    // @ts-expect-error not existing name
    currentState.name === 'unknown';

    if (currentState.name === 'notFound') {
      currentState.params;
      // @ts-expect-error "params" values are not available
      currentState.params.unknown;
      currentState.query;
      // @ts-expect-error "query" values are not available
      currentState.query.unknown;
    }

    if (currentState.name === 'internalError') {
      currentState.params;
      // @ts-expect-error "params" values are not available
      currentState.params.unknown;
      currentState.query;
      // @ts-expect-error "query" values are not available
      currentState.query.unknown;
    }

    if (currentState.name === 'static') {
      currentState.params;
      currentState.query;
      // @ts-expect-error "params" values are not available
      currentState.params.unknown;
      // @ts-expect-error "query" values are not available
      currentState.query.unknown;
    }

    if (currentState.name === 'staticQuery') {
      currentState.query.q;
      // @ts-expect-error unknown "query" are not available
      currentState.query.unknown;
      // @ts-expect-error "params" values are not available
      currentState.params.unknown;
    }

    if (currentState.name === 'dynamic') {
      currentState.params.id;
      // @ts-expect-error unknown "params" are not available
      currentState.params.unknown;
      // @ts-expect-error "query" values are not available
      currentState.query.unknown;
    }

    if (currentState.name === 'dynamicQuery') {
      currentState.params.id;
      currentState.query.q;
      // @ts-expect-error unknown "params" are not available
      currentState.params.unknown;
      // @ts-expect-error unknown "query" are not available
      currentState.query.unknown;
    }
  },
});

This way, the user page can access its PageStore through globalStore.pages.user. This makes it possible to use code-splitting more efficiently and serialize only globalStore during SSR — it will already contain the data for the required page.

This function can also be used to interrupt async operations and subscriptions.

router.redirect

Performs the full redirect cycle, described in more detail in the how it works section. If you pass an additional replace: true property, the last browser history entry will be replaced. Returns a string with the new URL.

The second argument is skipLifecycle?: boolean if you need to skip beforeEnter and beforeLeave calls.

ts
const newUrl = await router.redirect({
  name: 'user',
  params: { id: '9999' },
  query: { phone: '123456' }
})
// '/user/9999?phone=123456'
StateDynamic typing tests
ts
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);

router.urlToState

Accepts a URL and returns State with a fallback to notFound.

ts
router.urlToState(`/user/9999?phone=123456&gtm=value`);

// {
//   name: 'user',
//   params: { id: '9999' },
//   query: { phone: '123456' }
// }

router.urlToState(`/not-existing/admin?hacker=sql-inject`);

// { 
//  name: 'notFound', 
//  params: {}, 
//  query: {}
// }

INFO

Only the described query values that pass validation are preserved; in this case, gtm does not end up in State.

router.init

Short form of router.redirect(router.urlToState(url)). The second argument is skipLifecycle?: boolean if you need to skip beforeEnter and beforeLeave calls.

ts
// browser usage example:
await router.init(location.href)

// browser usage with SSR example:
await router.init(location.href, { skipLifecycle: true })

// Express.js server usage example:
const newUrl = await router.init(req.originalUrl)

// Optional step if you want to clear irrelevant query
if (req.originalUrl !== newUrl) res.redirect(newUrl)

router.state

Reactive object whose keys are name and whose values are State, for example:

ts
console.log(router.state.user);

// reactive object
// {
//   name: 'user',
//   params: { id: '9999' },
//   query: { phone: '123456' }
// }

Intended for displaying values in the UI and for describing logic in autoruns/effects. On redirect with new params or query, these values will change accordingly in router.state.user.

The router does not destroy the old State when navigating to another Config. In this example, if you navigate to router.redirect({ name: 'home' }), router.state.user will still be present. This helps solve the problem of uncleared subscriptions to old state at runtime.

If Reactive Route stored only one active router.getActiveState() (this method does not exist!), like many non-reactive routers do, then the subscription would start before component unmount with an incorrect State in which these parameters might be missing.

tsx
// with mobx adapter

import { autorun } from 'mobx'

function PageUser() {
  const { router } = useContext(RouterContext);

  const pageState = router.state.user!;
  
  useEffect(() => autorun(() => {
    console.log(pageState.params.id, pageState.query.phone);
  }), []);
}
tsx
// with kr-observable adapter

import { autorun } from 'kr-observable'

function PageUser() {
  const { router } = useContext(RouterContext);

  const pageState = router.state.user!;
  
  useEffect(() => autorun(() => {
    console.log(pageState.params.id, pageState.query.phone);
  }), []);
}
tsx
// with solid adapter

import { createRenderEffect } from 'solid-js'

function PageUser() {
  const { router } = useContext(RouterContext);

  const pageState = router.state.user!;

  createRenderEffect(() => {
    console.log(pageState.params.id, pageState.query.phone);
  })
}
vue
<script lang="ts" setup>
  // with vue adapter
  
  import { watchEffect } from 'vue';

  const { router } = useRouterStore();

  const pageState = router.state.user!;

  watchEffect(() => {
    console.log(pageState.params.id, pageState.query.phone);
  });
</script>

router.isRedirecting

Reactive boolean for displaying loading indicators during redirects. Examples of global and local display are shown below:

tsx
function GlobalLoader() {
  const { router } = useContext(RouterContext);

  return router.isRedirecting ? <Loader/> : null;
}

function SomeComponent() {
  const { router } = useContext(RouterContext);

  return <Button isLoading={router.isRedirecting}>Send</Button>;
}
tsx
function GlobalLoader() {
  const { router } = useContext(RouterContext);

  return router.isRedirecting ? <Loader/> : null;
}

function SomeComponent() {
  const { router } = useContext(RouterContext);

  return <Button isLoading={router.isRedirecting}>Send</Button>;
}
tsx
function GlobalLoader() {
  const { router } = useContext(RouterContext);

  return <Show when={router.isRedirecting}><Loader/></Show>;
}

function SomeComponent() {
  const { router } = useContext(RouterContext);

  return <Button isLoading={router.isRedirecting}>Send</Button>;
}
vue
<script lang="ts" setup>
  import { useRouter } from '../../router';

  const { router } = useRouter();
</script>

<template>
  <Loader v-if="router.isRedirecting"/>

  <Button :is-loading="router.isRedirecting">Send</Button>
</template>

router.activeName

Reactive name of the active State (undefined until the very first redirect).

router.preloadComponent

Reactive Route loads page chunks (executes loader) only during redirects. This function can be used for preloading and accepts name

ts
await router.init(location.href);

setTimeout(async () => {
  try {
    await router.preloadComponent('login')
    await router.preloadComponent('dashboard')
  } catch (e) {
    console.error('Seems like the user lost connection')
  }
}, 5000)

router.getGlobalArguments

Allows you to get the configuration passed to createRouter.

No AI participated in the development. MIT License