Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Minimalist routing in Redux

Why would you want to do this? Because you often don't need more. It's nice to not have to think about your "router" as this big special thing.

Instead, with this approch, your app's current pathname is just another piece of state, just like anything else.

This also means that when doing server-side rendering of a redux app, you can just do:

var app = require('your/redux/app')
var React = require('react')

// pass the URL like any other piece of data
React.renderToString(app.render({url: '/about'}))

The react-internal-nav component makes sure internal navigation clicks are captured, then we turn it into an SET_URL action dispatch.

This example has extremely simple url matching with just a few if statements, but this could easily be elaborated on if you needed it, using some kind of lightweight string route matcher, such as:

While standalone history management tools exist, such as https://browserstate.github.io/history.js/demo/ or https://github.com/rackt/history they're concerned with proper handling the state part of pushState APIs. Personally, I don't want to put state there anyway and if you don't care about that part, the actual browser support for plain pushState is quite good.

import { SET_URL } from '../constants/ActionTypes'
function pushState(url) {
if (url !== window.location.pathname) {
window.history.pushState({}, '', url)
}
}
export function setUrl(text) {
pushState(text)
return {
type: SET_URL,
text
}
}
export const SET_URL = 'SET_URL'
/* global __IS_DEV__*/
import React from 'react'
import { createStore, combineReducers } from 'redux'
import { Provider } from 'react-redux'
import FooApp from './FooApp'
import * as reducers from '../reducers'
import * as UrlActions from '../actions/UrlActions'
export default class App {
// note that we take data as an argument here
render (data) {
// ...and pass it through as initial state for server rendering
let store = createStore(createStore(reducers), data)
if (typeof window !== 'undefined') {
window.addEventListener('popstate', () => {
store.dispatch(UrlActions.setUrl(window.location.pathname))
})
store.dispatch(UrlActions.setUrl(window.location.pathname))
}
return (
<Provider store={store}>
{() => <FooApp />}
</Provider>
)
}
}
import React from 'react'
import { bindActionCreators } from 'redux'
import { Connector } from 'react-redux'
import InternalNav from 'react-internal-nav'
import AboutPage from '../components/about-page'
import DataPage from '../components/data-page'
import PersonDetail from '../components/person-detail'
import * as UrlActions from '../actions/UrlActions'
export default class FooApp {
render() {
return (
<Connector>
{this.renderChild}
</Connector>
)
}
renderChild({ personData: {persons}, dispatch, url }) {
const actions = {
...bindActionCreators(UrlActions, dispatch)
}
let page
// more sophisticated url matching could easily be done with any of these:
// https://github.com/Matt-Esch/http-hash
// https://github.com/glassresistor/i40
// https://github.com/bevacqua/ruta3
if (url === '/') {
page = (<h1>Home</h1>)
} else if (url === '/about') {
page = (<AboutPage/>)
} else if (url === '/data') {
page = (<DataPage persons={persons}/>)
} else {
let match
persons.some(person => {
if (person.slug === url.slice(1)) {
match = person
return true
}
})
if (match) {
page = (<PersonDetail person={match} {...actions}/>)
} else {
page = (<h1>404</h1>)
}
}
return (
<InternalNav onInternalNav={actions.setUrl}>
<header>
<nav role='navigation'>
<a href='/'>Example.com</a>
<span>
<a href="/data">Notes</a>
<a href="/about">About</a>
</span>
</nav>
</header>
{page}
</InternalNav>
)
}
}
@btholt
Copy link

btholt commented Aug 11, 2015

Really cool. Thanks for sharing.

@HenrikJoreteg
Copy link
Author

HenrikJoreteg commented Aug 11, 2015

@btholt np, I feel like this is one area that we tend to massively over-engineer.

@HenrikJoreteg
Copy link
Author

HenrikJoreteg commented Aug 11, 2015

I'm sure there's typos and such, just trying to convey the idea. I copied this out of an existing app and deleted a bunch of stuff that wasn't directly related to routing. Quite possible it doesn't run as written. But you get the idea :)

@kwhitaker
Copy link

kwhitaker commented Aug 11, 2015

Wow this is slick

@ream88
Copy link

ream88 commented Aug 11, 2015

I built something very similar for an old school Flux app (http://cassandra.build) and I never felt where to put the routing logic. Into a store, action, some util? But it's so clear for redux! Thx @gaearon and all other folks for their awesome work!

@Agamennon
Copy link

Agamennon commented Aug 30, 2015

I was experimenting with this idea a while now, i have created the redux-tiny-router please send in feedback

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment