A Redux First Router Saga

August 09, 2017 0 Comments

A Redux First Router Saga

 

 

Routing in React never sat well with me. Any solution that has you declare your routing with React components is asking React to manage concerns that are not related to the view layer.

I firmly believe that routes are simply state; that the fact that they usually map to browser URLs is simply a side-effect, and as such they should not be managed within our React code. With this in mind I set out to create a solution that matches this way of thinking.

At first I used a hand-rolled redux-saga system to manage all routing with sagas. This achieves the desired effect of allowing complete control over showing loaders, loading data, and then loading our views. Ultimately this more tightly coupled the view layer to my sagas than I would have liked.

Then along came redux-first-router, and a better way of doing things. The end-goal is simple: have views be aware of how they map to a particular route, have sagas be aware of how they map to a particular route, then simply define the routes as state. Something like this:

src/
|- state/
| |_ sagas/
| | |_ routes.js // Our routes to sagas mapping.
| |
| |_ routes.js // Our routes to state definitions.
|
|_ view/
|_ Routes.js // Our routes to views mapping.

When defining our system this way we gain a few things:

  • Decoupling our views and side-effects from each other.
  • The ability to have routes that have no UI, such as an authentication callback, and logically not have the view layer be aware that such a route exists.
  • We can change our actual URLs by updating a simple map inside state/routes.js without having to manipulate our sagas or React components.

Let’s start with the library wiring. For the purposes of this tutorial we will assume that you have the necessary dependencies installed. All the code used in this article can be found on Github at bfillmer/redux-routing-and-more, and can be played with at https://codesandbox.io/s/Y6v89MErM.

src/state/routes.js

import {connectRoutes} from 'redux-first-router'
import createHistory from 'history/createBrowserHistory'

const {ROUTEHOME, ROUTEABOUT} from 'types'

const routesMap = {
[ROUTEHOME]: '/',
[ROUTE
ABOUT]: '/about'
}

const history = createHistory()

export const {
reducer,
middleware,
enhancer
} = connectRoutes(history, routesMap)

We would like to work in a type-based system, so we define some types that represent our routes. These will be used later by our sagas and views to load the appropriate parts of our application. After this we create and export our reducer, middleware, and enhancers from redux-first-router. We won’t go into why all three are created or how they are connected to the store, that information can be found in the redux-first-router documentation.

src/view/Routes.js

import React from 'react'
import {connect} from 'react-redux'
import {NOTFOUND} from 'redux-first-router'

import {routeType} from 'selectors'
import {ROUTE
HOME, ROUTEABOUT} from 'types'
// View Components
import {About} from 'view/About'
import {Home} from 'view/Home'

const routesMap = {
[ROUTE
HOME]: Home,
[ROUTEABOUT]: About,
[NOT
FOUND]: Home
}

const mapStateToProps = state => ({
route: state => state.location.type
})

const Container = ({route}) => {
const Route = routesMap[route]
? routesMap[route]
: routesMap[NOTFOUND]
  return (
<Route />
)
}

export const Routes = connect(mapStateToProps)(Container)

Now we map our view components to our types. We use the special NOTFOUND type from redux-first-router so we always have some UI to show regardless of our current application state. Add some Redux wiring to get the current state type and we are done. Our App.js file simply loads our <Routes />:

src/view/App.js

import React from 'react'

import {Routes} from 'view/Routes'

export const App = () => (
<Routes />
)

In the case that we might have multiple areas of our UI respond to route changes differently, say a primary content area and a sidebar, we simply create multiple maps of types to views and easily gain independently updating UI areas.

At this point routing is viable in our application, booting up and going to our two routes, / and /about will render our Home and About components as expected. Quite often our applications are not quite that simple, and we need to perform some type of data loading or other state management either before or concurrently with loading our view.

src/state/sagas/routes.js

import {spawn, takeEvery} from 'redux-saga/effects'

import {ROUTEHOME} from 'types'

// Route Sagas
import {loadHome} from 'state/sagas/home'
// Routes that require side effects on load are mapped here.
const routesMap = {
[ROUTE
HOME]: loadHome
}

// Spawn the saga associated with the route type.
function * handleRouteChange ({type}) {
yield spawn(routesMap[type])
}

// Watch for all actions dispatched that have an action type in
// our routesMap.
export function * routes () {
yield takeEvery(Object.keys(routesMap), handleRouteChange)
}

This saga watches for any actions that are dispatched to Redux that match an action type mapped in our routeMap. If one exists it creates a non-blocking thread (of sorts) and then resumes watching for the next action type. The beauty of this system is that we can now pick and choose what routes we need to have side-effects for and instantiate them by coupling to the correct route type. No touching our view layer, or binding directly to the url we happen to be using at the time.

src/state/sagas/home.js

import {delay} from 'redux-saga'
// Here we would do checks for existing data and load whatever
// we need for this view. Also manage generic tasks such as
// showing/hiding loaders based on UI needs.
export function * loadHome () {
yield delay(1000)
yield console.log('Just Finished a Super Long API Call')
}

Finally the loadHome saga takes over and is fully in charge of what should happen in order to retrieve whatever data we require when the ROUTE_HOME action has fired. This could include triggering a state change that displays a loader, checking if some data exists, loading said data if it doesn’t, and hiding the loader.

This pattern is the best way I have found so far to handle routing, side-effects, and cleanly coupling the various pieces involved in routing with a React-based application. Many thanks to James Gillmore for creating redux-first-router.

All the code for this tutorial can be found on Github at bfillmer/redux-routing-and-more and played with at https://codesandbox.io/s/Y6v89MErM.


Tag cloud