Skip to main content

router

The kea-router plugin provides a nice wrapper around window.History and helps manage the URL in your application. Use it to listen to route changes or change the URL yourself. There are a few helpers (actionToUrl and urlToAction) that help track the URL changes, or access the router directly to manually control the browser history object.

Installation

First install the kea-router package:

# if you're using yarn
yarn add kea-router

# if you're using npm
npm install --save kea-router

Then install the plugin:

import { routerPlugin } from 'kea-router'
import { resetContext } from 'kea'

resetContext({
plugins: [
routerPlugin({
/* options */
}),
],
})

Configuration options

The plugin takes the following options:

routerPlugin({
// The browser History API or something that mocks it
// Defaults to window.history in the browser and a mock memoryHistory otherwise
history: window.history,

// An object with the keys { pathname, search, hash } used to
// get the current location. Defaults to window.location in the browser and
// an empty object otherwise.
location: window.location,

// If there is a difference between the path in the browser and the path in
// your routes, use these functions to clear it up.
// For example to have the same app on many subfolders in one the site.
pathFromRoutesToWindow: (path) => '/subfolder' + path,
pathFromWindowToRoutes: (path) => path.replace(/^\/subfolder/, ''),

// kea-router has support for (de)serializing the search and hash parameters
// It comes with sensible default functions, yet you can override them here
encodeParams: (obj = { key: 'value' }, symbol = '?') => '?key=value',
decodeParams: (input = '?key=value', symbol = '?') => ({ key: 'value' }),

// Passed directly to url-pattern.
urlPatternOptions: {
// What characters to match as ":key" with "/url/:key"
// You must set this explicitly if you need to match a "." or a "@"
segmentValueCharset: "a-zA-Z0-9-_~ %.@()!'",
},
})

Sample usage

Use actionToUrl to change the URL in response to actions and urlToAction to dispatch actions when the route changes

import { kea } from 'kea'
import { actionToUrl } from 'kea-router'

export const articlesLogic = kea([
actionToUrl(({ values }) => ({
openList: ({ id }) => `/articles`,
openArticle: ({ id }) => `/articles/${id}`,
openComments: () => `/articles/${values.article.id}/comments`,
closeComments: () => `/articles/${values.article.id}`,
})),

urlToAction(({ actions }) => ({
'/articles': () => actions.openList(),
'/articles/:id(/:extra)': ({ id, extra }) => {
actions.openArticle(id)
if (extra === 'comments') {
actions.openComments()
} else {
actions.closeComments()
}
},
})),

// Skipped in the examples
actions({}),
reducers({}),
selectors({}),
])

Url Pattern

kea-router uses the url-pattern library under the hood to match paths. Please see its documentation for all supported options.

UrlToAction

Search and Hash parameters

kea-router has built in support for serializing and deserializing search and hash URL parameters, such as:

// "pathname" + "?search" + "#hash"
url = 'http://example.com/path?searchParam=true#hashParam=nah'

The second and third parameters to urlToAction are searchParams and hashParams respectively. These are deserialized objects that you can use directly.

Full example

import { kea } from 'kea'

export const articlesLogic = kea([
urlToAction(({ actions }) => ({
// Synax:
// urlToAction: ({ actions }) => ({
// '/path': (pathParams, searchParams, hashParams, payload) => {
// // ...
// }
// })
//
// Example on url: "/articles?id=123&comments=true#hashKey=hurray"
// --> pathParams = {}
// --> searchParams = { id: 123, comments: true }
// --> hashParams = { hashKey: 'hurray' }
// --> payload = // payload for router.actions.locationChanged
'/articles': (_, { id, comments }, { hashKey }) => {
if (id) {
actions.openArticle(id)
if (comments) {
actions.openComments()
} else {
actions.closeComments()
}
} else {
actions.openList()
}
},
})),
])

For actionToUrl, you may include the search and hash parts directly in the URL or return an array in the format: [pathname, searchParams, hashParams]. The searchParams and hashParams can be both strings or objects.

import { kea } from 'kea'

export const articlesLogic = kea([
actionToUrl(({ values }) => ({
// Use one of:
// - action: () => url,
// - action: () => [url, searchParams, hashParams],

openList: ({ id }) => `/articles`,

// these three are equivalent
openArticle: ({ id }) => `/articles?id=${id}`,
openArticle: ({ id }) => [`/articles`, { id }],
openArticle: ({ id }) => [`/articles`, `?id=${id}`],

openComments: () => [`/articles`, { id: values.article.id, comments: true }],
closeComments: () => [`/articles`, { id: values.article.id }, '#hashKey=true'],
})),
])

Control the route directly

Import router to control the router directly in your components

import React from 'react'
import { useActions, useValues } from 'kea'
import { router } from 'kea-router'

export function MyComponent() {
const { push, replace } = useActions(router)
const {
location: { pathname, search, hash }, // strings
searchParams, // object
hashParams, // object
} = useValues(router)

return (
<div>
{pathname === '/setup' ? <Setup /> : <Dashboard />}
<button onclick={() => push('/setup')}>Open Setup</button>
</div>
)
}

Or in a logic:

import { kea } from 'kea'
import { router } from 'kea-router'

const logic = kea([
actions({
buttonPress: true,
}),
listeners({
buttonPress: () => {
if (router.values.location.pathname !== '/setup') {
router.actions.push('/setup', { search: 'param' }, '#integration')
}
},
}),
])

Both the push and replace actions accept searchParams and hashParams as their second and third arguments. You can provide both an object or a string for them. You can also include the search and hash parts in the url.

Use the included <A> tag to link via the router. This changes the URL via router.actions.push() instead of reloading the entire page.

import React from 'react'
import { A } from 'kea-router'

// use <A href=''> instead of <a href=''> to open links via the router
export function Page() {
return (
<ul>
<li>
<A href="/about">About me</A>
</li>
<li>
<A href="/contact">Contact</A>
</li>
</ul>
)
}

The <ActiveA /> tag will add an active class to all links that are currently active.

import React from 'react'
import { ActiveA } from 'kea-router'

export function Page() {
return (
<ul>
<li>
<ActiveA href="/about">About me</ActiveA>
</li>
<li>
<ActiveA href="/contact">Contact</ActiveA>
</li>
</ul>
)
}

Listen to location changes

In case urlToAction is not sufficient for your needs, listen to the locationChanged action to react to URL changes manually:

import { kea } from 'kea'
import { router } from 'kea-router'

const otherLogic = kea([
listeners({
[router.actions.locationChanged]: ({ pathname, search, hash, method }) => {
console.log({ pathname, search })
},
}),
])

Global scene router

Here's sample code for a global scene router

import React, { lazy } from 'react'

export const scenes = {
error404: () => <div>404</div>,
dashboard: lazy(() => import('./dashboard/DashboardScene')),
login: lazy(() => import('./login/LoginScene')),
projects: lazy(() => import('./projects/ProjectsScene')),
}

export const routes = {
'/': 'dashboard',
'/login': 'login',
'/projects': 'projects',
'/projects/:id': 'projects',
}

export const sceneLogic = kea([
actions({
setScene: (scene, params) => ({ scene, params }),
}),
reducers({
scene: [
null,
{
setScene: (_, payload) => payload.scene,
},
],
params: [
{},
{
setScene: (_, payload) => payload.params || {},
},
],
}),
urlToAction(({ actions }) => {
return Object.fromEntries(
Object.entries(routes).map(([path, scene]) => {
return [path, (params) => actions.setScene(scene, params)]
})
)
}),
])

export function Layout({ children }) {
return (
<div className="layout">
<div className="menu">...</div>
<div className="content">{children}</div>
</div>
)
}

export function Scenes() {
const { scene, params } = useValues(sceneLogic)

const Scene = scenes[scene] || scenes.error404

return (
<Layout>
<Suspense fallback={() => <div>Loading...</div>}>
<Scene {...params} />
</Suspense>
</Layout>
)
}

Utility functions

kea-router exposes three functions to help manage urls in your app:

import { encodeParams, decodeParams, combineUrl } from 'kea-router'

// Use `encodeParams` to convert an object to part of a path
// --> encodeParams(object, symbol)
encodeParams({ key: 'value' }, '?') === '?key=value'

// Use `decodeParams` to convert a part of a path to an object
// --> decodeParams(object, symbol)
decodeParams('?key=value', '?') === { key: 'value' }
decodeParams('key=value', '?') === { key: 'value' }

// Use `combineUrl` to both split an existing url into its components and
// to merge new search and hash parts into an existing url.
// --> combineUrl(url, searchInput, hashInput, encodeParams, decodeParams)
// - `searchInput` and `hashInput` can be either a string or an object
// - `encodeParams` and `decodeParams` can be overridden if needed
combineUrl('/path?key=value#hash') ===
{
url: '/path?key=value#hash',
pathname: '/path',
search: '?key=value',
searchParams: { key: 'value' },
hash: '#hash',
hashParams: { hash: null },
}
combineUrl('/path?key=value#hash', { key: 'otherValue' }, '#addHash=bla') ===
{
url: '/path?key=otherValue#hash&addHash=bla',
pathname: '/path',
search: '?key=otherValue',
searchParams: { key: 'otherValue' },
hash: '#hash&addHash=bla',
hashParams: { hash: null, addHash: 'bla' },
}

beforeUnload

To trigger an alert if the user changes the route, use:

kea([
beforeUnload(({ actions, values }) => ({
enabled: (newLocation?: CombinedLocation) => values.formChanged,
message: 'Your changes will be lost. Are you sure you want to leave?',
onConfirm: () => actions.resetForm(),
})),
])

The enabled function will only get the newLocation argument if the user navigates within the single page app. If the user navigates away from the app, for example by clicking an external link, or clicking "back" enough times, we will not know where they are going. The browser doesn't reveal this for privacy reasons.

Questions & Answers

Ask questions about this page here.