zapier/redux-router-kit

Name: redux-router-kit

Owner: Zapier

Description: Routing tools for React+Redux

Created: 2016-03-30 15:45:13.0

Updated: 2017-11-23 19:47:06.0

Pushed: 2018-01-10 16:55:50.0

Homepage:

Size: 196

Language: JavaScript

GitHub Committers

UserMost Recent Commit# Commits

Other Committers

UserEmailMost Recent Commit# Commits

README

Redux Router Kit

travis

Redux Router Kit is a routing solution for React that leverages Redux to store routing and transition states and enable powerful middleware and connect-powered components.

Version 1.0.0 requires React 16+

Features
Why not React Router?

If the features above aren't useful, by all means, use React Router instead! It's battle-tested, and Redux Router Kit borrows its concepts heavily! Redux Router Kit is an alternative that gives tighter integration with Redux.

Is this thing ready?

Well, it's been used in production for https://zapier.com for a while now. So for us, it's ready. :-) For your use case, you may find edges. If so, let us know!

Install
install redux-router-kit --save
Basic usage
rt ReactDOM from 'react-dom';
rt { combineReducers, applyMiddleware, Provider } from 'react-redux';
rt {
uterReducer, createRouterMiddleware, RouterHistoryContainer
om 'redux-router-kit';

t HomePage = () => (
iv>Home!</div>


t TodoApp = ({params}) => (
rams.id ? (
<div>Todo: {params.id}</div>
: (
<div>Todos: {/* list todos */}</div>



ou can point route paths directly to components in simple cases.
t routes = {
': HomePage,
todos': TodoApp,
todos/:id': TodoApp


t reducer = combineReducers({
uter: routerReducer


t store = createStore(
ducer,
plyMiddleware(
createRouterMiddleware({routes})



t Root = createReactClass({
nder() {
return (
  <RouterHistoryContainer routes={routes}/>
)



tDOM.render(
rovider>
<Root/>
Provider>
cument.getElementById('app')

Nested routes, onLeave/onEnter, and assign
t Layout = ({children}) => (
iv>
<header>The Header</header>
<div>{children}</div>
div>


t HomePage = () => (
iv>Home!</div>


t TodoApp = ({children}) => (
iv>{children}</div>


t TodoList = () => (
iv>Todos: {/* list todos */}</div>


t TodoItem = ({params}) => (
iv>Todo: {params.id}</div>


t routes = {
': {
component: Layout,
routes: {
  // This is the "index" route. (Like a current directory.)
  '.': {
    component: HomePage
  },
  '/todos': {
    component: TodoApp,
    routes: {
      '.': {
        component: TodoList,
        assign({query}) {
          if (query.page) {
            // Return any properties for `routing.next`/`routing.current`.
            // These can be new ad-hoc properties, or modifications of
            // params or query.
            return {
              query: {
                ...query,
                // Convert page to an integer.
                page: parseInt(query.page)
              }
            };
          }
        }
      },
      'new': {
        onEnter({routeTo}) {
          // This might be a terrible example, if this is slow.
          return createTodo()
            .then(todo => {
              routeTo(`/todos/${todo.id}`);
            })
        }
      },
      ':id': {
        component: TodoItem,
        onLeave({router, cancelRoute, getState, dispatch}) {
          const todo = getState().todos[router.current.params.id];
          if (!todo.isSaved) {
            cancelRoute();
            dispatch(notifyToSaveTodo());
          }
        }
      }
    }
  }
}


Router / RouterContainer / RouterHistoryContainer

The Router component requires a routing prop with routing state from the store.

The RouterContainer component is connected to the store, and so automatically gets that prop.

The RouterHistoryContainer component adds in a History component to update browser address state and automatically dispatch routing actions when the browser history changes.

All these components accept the following props.

routes

Route mapping object. See the examples above.

renderBeforeCurrent({router})

If there is no current route, this function will be called.

render({router, query, params, matchedRoutes})

If you'd like to take control of all rendering for routes, pass in this function. No other rendering functions will be called. If no routes match, then matchedRoutes will be null.

renderRoutes({router, query, params, matchedRoutes})

Like render, but only called if there are matchedRoutes.

renderDefault({router, query, params, matchedRoutes})

If the matching routes don't have any components or don't reduce to a single element, this function will be called.

renderRoot({router, query, params, matchedRoutes})

After all components have reduced to a single element (or map of named elements), this function will be called to render any wrapping elements.

createElement(Component, {router, query, params, matchedRoutes, route, children})

For each component in a route, this function is called to return an element to be rendered. If child routes provide named components, named elements will be passed as props instead of children.

Routing component props

Components rendered by routes receive the following props. These will also be passed to createElement if you provide that function to Router/RouterContainer/RouterHistoryContainer. (As well as the other render callbacks listed above.)

router

This is the current routing state. An example of the routing state is:


 When the url changes, `current` moves to `previous`.
evious: {
url: '/todos/123',
// ... same properties as current

rrent: {
url: '/todos/123?edit=true',
query: {
  edit: 'true'
},
params: {
  id: 123
},
routeKey: ['/todos', ':id'],
location: {
  host: 'www.example.com',
  pathname: '/todos/123',
  protocol: 'https:',
  // etc., just like browser's location
},
replace: false,
state: null

 When the url changes, `next` will first get the new value of `current`.
 Middleware or components can then cancel or redirect. If not canceled
 or redirected, `current` will then become `next`. If `next` is null,
 there is no current transition.
xt: null

matchedRoutes

An array of matched routes.

route

The specific route being rendered.

params

The route parameters.

query

The query parameters.

Links

When you use RouterHistoryContainer, it responds to click/touch events so routing actions are automatically triggered. So you don't have to use a special <Link> component. A normal <a> will work just fine.

Routing action creators
routeTo(url, {event, replace, exit})

Returns a ROUTE_TO_NEXT action, which, when dispatched, adds url to router.next state. Calls onLeave hooks for any routes which are removed and onEnter hooks for any routes which are added.

The route can be canceled with cancelRoute or redirected or exited with another routeTo.

If event is provided, it will be inspected for things like command-click to open new tabs.

If exit is provided, the route will roughly be equivalent to:

ow.location.href = url

(Currently, only absolute urls are supported though.)

cancelRoute()

Cancels the router.next route and removes it from state.

Dispatching routing actions from components

If you do want to manually trigger routing actions, you can either manually wire up the action with connect:

rt { routeTo } from 'redux-router-kit';

t AddTodoButton = ({routeTo}) => (
utton onClick={() => routeTo('/todos/new')}>Add New Todo</button>


t ConnectedAddTodoButton = connect(
ll,
ispatch) => {
routeTo(..args) {
  dispatch(routeTo(...args));
}

dTodoButton);

Or you can use the included connectRouterActions to add the actions as props.

rt { connectRouterActions } from 'redux-router-kit';

t AddTodoButton = ({routeTo}) => (
utton onClick={() => routeTo('/todos/new')}>Add New Todo</button>


t ConnectedAddTodoButton = connectRouterActions(AddTodoButton);

If you only need routeTo (because you typically don't need cancelRoute), then you can use connectRouteTo instead.

Connecting your components to routing state

You can use connect to grab any routing state for your components. For example:

t TodoItem = ({query, todo}) => {
nst style = query.theme === 'dark' ? {
color: 'white',
backgroundColor: 'black'
: {};
turn <div style={style}>{todo.title}</div>;


t TodoItemContainer = connect(
ate => ({
query: state.router.current.query

doItem);

You can also use connectRouter to grab all routing state and action creators for your components. For example:

t TodoItem = ({router, todo}) => {
nst style = router.current.query.theme === 'dark' ? {
color: 'white',
backgroundColor: 'black'
: {};
turn <div style={style}>{todo.title}</div>;


t TodoItemContainer = connectRouter(TodoItem);

You should only use this if you want your component to be updated for all routing state changes. For example, the second example will update during routing transition, whereas the second will only update when the current route is changed.

Custom middleware

Here's an example of custom middleware that would require the user to login.

rt { ROUTE_TO_NEXT, findRoutes, routeTo } from 'redux-router-kit';

t routes = {
': {
component: HomePage

me': {
component: AccountDetails,
requiresLogin: true



t createLoginMiddleware = ({routes}) => {
nst middleware = store => next => action => {
if (!action || !action.type) {
  return next(action);
}

if (!action.type === ROUTE_TO_NEXT) {
  const matchedRoutes = findRoutes(routes, action.meta.routeKey);
  if (matchedRoutes.some(route => route.requiresLogin)) {
    const { account } = store.getState();
    if (!account.isLoggedIn) {
      return dispatch(routeTo('/login'));
    }
  }
}

return next(action);

turn middleware;


t store = createStore(
ducer,
plyMiddleware(
createRouterMiddleware({routes}),
createLoginMiddleware({routes})


Async route loading

To load routes asynchronously, just add a fetch property to your route.

t routes = {
': HomePage,
todos': TodoApp,
developer': {
// This is a big page, and we don't want it loaded for everyone.
fetch() {
  return System.import('developerRoutes');
}


The result of the fetch will be used in place of that route. The routing table in middleware will be modified with the new route, and the url will be retried against the new routing table. (And any nested async routes will also be fetched.) If you need the routing table outside middleware, you can listen to changes.

t routerMiddleware = createRouterMiddleware({routes});

t store = createStore(
ducer,
plyMiddleware(
routerMiddleware



erMiddleware.onRoutesChanged(routes => {
 do something with these routes, like pass them to components or other middleware that need them

If you'd like to be in control of fetching routes, you can pass a fetchRoute function into the middleware.

t routes = {
': HomePage,
todos': TodoApp,
developer': {
// Can be any truthy value.
fetch: true



t fetchRoute = route => {
 Return fetched route.


t routerMiddleware = createRouterMiddleware({routes, fetchRoute});
Async route loading state

While loading async routes, router.fetch will be set in state. Because routes aren't yet loaded, the params/etc. will be incomplete.

Server-side/static rendering

For server-side or static rendering, just use RouterContainer instead of RouterHistoryContainer.

t Home = createReactClass({
nder() {
return <div>Home</div>;


t routes = {
': Home

t store = createStore(
mbineReducers({
router: routerReducer
,
plyMiddleware(
createRouterMiddleware({routes})


rn store.dispatch(routeTo('/'))
hen(() => {
const htmlString = renderToStaticMarkup(
  <Provider store={store}>
    <RouterContainer routes={routes}/>
  </Provider>
);
// htmlString is now: <div>Home</div>
;
Thanks!

Redux Router Kit heavily borrows ideas from React Router (https://github.com/reactjs/react-router).

The History component borrows heavily from https://github.com/cerebral/addressbar and https://github.com/christianalfoni/react-addressbar.

Internally, history (https://github.com/mjackson/history) is used, and it's pretty awesome that it's separate from React Router. :-)


This work is supported by the National Institutes of Health's National Center for Advancing Translational Sciences, Grant Number U24TR002306. This work is solely the responsibility of the creators and does not necessarily represent the official views of the National Institutes of Health.