This documentation is for the old Kea 1.0. To see the latest docs for 2.0, click here!

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 and kea-listeners packages:

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

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

Then install the plugin:

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

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

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/, '')
})

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'

export const articlesLogic = kea({
  // define the actions from below
  actions: () => ({ ... }),

  // define article = { id, ... }
  reducers: () => ({ ... }),

  actionToUrl: ({ actions, values }) => ({
    [actions.openList]: ({ id }) => `/articles`,
    [actions.openArticle]: ({ id }) => `/articles/${id}`,
    [actions.openComments]: () => `/articles/${values.article.id}/comments`,
    [actions.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()
      }
    },
  })
})

Import router to control the router directly

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 } } = useValues(router)

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

Create an A tag to make linking easier

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

// use <A href=''> instead of <a href=''> to open links via the router
export function A (props) {
  return (
    <a
      {...props}
      onClick={e => {
        if (!props.target) {
          e.preventDefault()
          router.actions.push(props.href) // router is mounted automatically, so this is safe to call
        }
      }}
    />
  )
}

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 })
    }
  })
})

Here's sample code for a global scene router

import React, { lazy, useMemo } from 'react'
import { router } from 'kea-router'

export const scenes = {
  'dashboard': () => import(/* webpackChunkName: 'dashboard' */'./dashboard/DashboardScene'),
  'login': () => import(/* webpackChunkName: 'login' */'./login/LoginScene'),
  'projects': () => import(/* webpackChunkName: 'projects' */'./projects/ProjectsScene'),
}

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

export const sceneLogic = kea({
  actions: () => ({
    setScene: (scene, params) => ({ scene, params })
  }),
  reducers: ({ actions }) => ({
    scene: [null, {
      [actions.setScene]: (_, payload) => payload.scene
    }],
    params: [{}, {
      [actions.setScene]: (_, payload) => payload.params || {}
    }]
  }),
  urlToAction: ({ actions }) => {
    const mapping = {}
    for (const [paths, scene] of Object.entries(routes)) {
      for (const path of paths.split('|')) {
        mapping[path] = params => actions.setScene(scene, params)
      }
    }
    return mapping
  }
})

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 = useMemo(() => {
    return scenes[scene] ? lazy(scenes[scene]) : () => <div>404</div>
  }, [scene])

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