@touchtribe/redux-router

1.8.0 • Public • Published

@touchtribe/redux-router

redux-router is a basic router for Redux applications.

While React Router is a great library, it makes it difficult to combine it with Redux patterns and near impossible to integrate it with Redux middlewares.

The router is inspired by redux-little-router. To understand the reasoning behind most of this router, check: "Let the URL do the Talking": Part 1 Part 2 Part 3

The principles behind the router are:

  • The URL is just another member of the state tree
  • URL changes are just plain actions
  • Route definitions should be easily customizable without having an impact on the actual implementation
  • Route definitions should be reusable throughout the app
  • Route definitions should come from a single source of truth

Usage

The router uses:

  • a middleware for:
    • intercepting navigation actions and calling their equivalent method in 'history'
    • intercepting URL changes, parsing them based on the specified patterns and dispatching locationChanged actions
  • a reducer for updating the state on URL changes
  • patterns for parsing the URL and translating it into a meaningful state-update
  • a Route component which renders children based on current route
  • a Link component which dispatch navigation actions when clicked

Middleware

createMiddleware(history, patterns) The middleware is responsible for:

  • intercepting navigation actions and calling their equivalent method in history
  • intercepting URL changes and dispatching locationChanged action

Paremeters:

  • history is one of the types from the history library
  • patterns is either
    • an array of patterns
    • an object having patterns as values
import { createStore, applyMiddleware } from 'redux'
import { createBrowserHistory } from 'history'
import { createMiddleware, createPattern } from '@touchtribe/redux-router'
import rootReducer from './reducers'

const history = createBrowserHistory()

const patterns = {
  Home: createPattern('/'),
  Contact: createPattern('/contact')
}

const routerMiddleware = createMiddleware(history, patterns)

const store = createStore(
  rootReducer,
  {},
  applyMiddleware(routerMiddleware)
)

Often, you'll want to update state or trigger side effects after loading the initial URL to ensure compatibility with other store enhancers.

import { initializeLocation } from '@touchtribe/redux-router'

// ...after creating your store and initializing other side-effect handlers
store.dispatch(initializeLocation(history.location))

Reducer

The router reducer works on the namespace router, which can also be access through routerReducer.toString() or String(routerReducer).

import { combineReducers } from 'redux'
import { routerReducer } from '@touchtribe/redux-router'
const rootReducer = combineReducers({
  router: routerReducer
})
// or using the routerReducer.toString() method
const rootReducer = combineReducers({
  [routerReducer]: routerReducer
})

Patterns

Patterns are definitions for URLs, which gets parsed using url-pattern

let pattern = createPattern('/users/:id')

See the documentation of url-pattern on how to use them.

React bindings and usage

  • <Route> component which renders children based on current route
  • <Link> component which dispatch navigation actions when clicked

<Route />

<Route> components conditionally render when the current location matches their pattern.

The simplest version of a route renders the children when a url matches the pattern

import { Route } from '@touchtribe/redux-router'
...
<Route pattern={'/user/:id'}>
  <p>This will render if the URL matches "/user/:id"
</Route>

pattern can also be an instance of UrlPattern (createPattern), which can be usefull for reusing patterns that are defined early in the process

import { Route, createPattern } from '@touchtribe/redux-router'

let Patterns = {
  Home: createPattern('/'),
  UserOverview: createPattern('/users/'),
  UserDetail: createPattern('/users/:id')
}
...
<Route pattern={Patterns.Home}>
  <p>This will render if the route matches "/user/:id"
</Route>

The <Route> component also accepts a component prop to render if it matches:

const UserDetailPage = (props) => <div>User Detail Page</div>
...
<Route pattern={'/user/:id'} component={UserDetailPage} />

For more control, children also accepts a render-function

<Route pattern={'/user/:id'}>
{({location, pattern, match, ...props}) => (
  match
  ? <p>this will render if the URL matches "/user/:id"
  : <p>this will render otherwise</p>
)}
</Route>

The idea behind redux-router doesn't allow for nested routes, since every route should come from a single source. While <Route> components can be nested, the patterns don't stack.

<Route pattern={'/users'}>
  <p>This will be rendered if the URL matches "/users"</p>
  <Route pattern={'/:id'}>
    <p>This will be rendered if the URL matches "/:id"</p>
  </Route>
  <Route pattern={'/users/:id'}>
    <p>This will be rendered if the URL matches "/users/:id"</p>
  </Route>
</Route>

<Route> makes basic component-per-page navigation easy:

<React.Fragment>
  <Route pattern={'/'} component={HomePage} />
  <Route pattern={'/users'} component={UserOverviewPage} />
  <Route pattern={'/users/:id'} component={UserDetailPage} />
</React.Fragment>

<Link>

The link component is easy to use and renders as <a href="...">...</a> by default

import { Link } from '@touchtribe/redux-router'

<Link href="/" className="blue-link">
  Click me
</Link>

It can also be used together with a pattern to promote reusability

...
<Link href={Patterns.UserOverview}>
  To user overview
</Link>

To change props when a link is active (matched), you can use the activeProps prop

className is a special case, in which className gets concatenated with activeProps.className

<Link href="/" className="link" activeProps={{className: 'link--active'}}>
  To homepage
</Link>

To render a Link as a different component, you can use the as prop:

the Component needs to propogate onClick for this to work

...
<Link href={Patterns.UserOverview} as={Button}>
  To user overview
</Link>

To add parameters to the URL, the query prop can be used. These will both be added as params for the Pattern as well as to the query-string

...
const UserOverviewPage = createPattern('/users/:userId')
<Link href={UserOverviewPage} query={{userId: 10}}>
  To user detail
</Link>

To use replace instead of push, the replaceState can be used:

...
<Link href={Patterns.UserOverview} replaceState>
  To user overview
</Link>

Actions

@touchtribe/redux-router providers the following action creators for navigation:

import { push, replace, go, back, forward, locationChanged, initializeLocation } from '@touchtribe/redux-router';

// to push a new route
push('/')
// to push a new route with a query
push('/', { userId: 10 })
// to push a new route using a pattern
const HomePage = createPattern('/')
push(HomePage)
// to push a new route using a pattern with params
const UserDetailPage = createPattern('/user/:userId')
push(UserDetailPage, { userId: 10 })

// replace and push use the same parameters
// to replace your current route
replace('/')

// Navigates forwards or backwards
go(3)
go(-2)

// equivalent of the browser back button
back()
// equivalent of the browser forward button
forward()

All action-types can be retrieved by using <action>.toString() or String(<action>), e.g.: String(push). This is necessary to listen for them in either your side-effects or your reducers:

const reducer = (state, action) => {
  switch(action.type) {
    case String(push): 
      ...
    case String(replace): 
      ...
    case String(initializeLocation): 
      ...
    case String(locationChanged):
      ...
  }
  return state
}

Helpers

createPattern(href)

To create patterns for reuse within the application

parseLocation(location, patterns)

Matches the current location against the given patterns and returns the information which would be reflected in the state:

{
  location,
  pattern,
  matches,
  params,
  hash
}

Readme

Keywords

none

Package Sidebar

Install

npm i @touchtribe/redux-router

Weekly Downloads

1

Version

1.8.0

License

DBAD

Unpacked Size

473 kB

Total Files

6

Last publish

Collaborators

  • mnjongerius
  • doxick