View Agnostic Routing with Redux

Michael Tiller
15 min readJan 13, 2016

--

As I discussed elsewhere, I have a substantial code base in Angular 1.x and I was interested in migrating it toward React and Redux.

My initial concerns were with migrating the templates and ensuring type safety in rendering using a combination of React and TypeScript. That by itself seemed like a big win. But one of the things I wasn’t really happy with using Angular was testing. Now this may seem odd because lots of people gush about the amount of tooling around testing in Angular.

I admit there seems to be an emphasis on testing in the Angular world. But it didn’t support the kind of testing I wanted to do. Maybe it’s because I don’t live completely in a front-end world or maybe it’s because I just don’t know how to properly architect my tests. But when I wrote tests, it always seemed impossible to keep the view out of it.

What do I mean by that? Well, there were artifacts of the view all over the place. For example, I had to run my tests in a browser or at least using PhantomJS in order to bring the DOM in. My tests often involved dispatching events on specific DOM elements which I somehow had to identify in my tests. Some of the logic I was actually testing was interwoven into the view/UI code in ways I didn’t like. This whole approach was, at least for me, pretty brittle (i.e., tests would start failing when I made UI changes) and it took the fun and satisfaction out of testing when I had to constantly tweak tests as I refined my views.

Maybe I’m an idiot, but I just couldn’t figure out how to make these tools work for me. This really came to a head when I started looking at Redux. Redux was really attractive to me because it provides a synchronous and deterministic way to manage the entire application state. This seemed very promising to me as a way to try and isolate the logic of my application. Reducers are a great abstraction to build on. I found a few other patterns as
well
that I think are really useful to help keep application logic out of the view. But a central issue I ran into almost immediately for user interfaces was routing.

I was using ‘ui-router’ from ‘angular-ui’ and it has a nice model for representing states, nested states and transitions. But it is very much tied to angular. I was also aware of `react-router` which provides routing on the React side of things (but was in a state of flux at the time). However, the issue I had with both of these was that they were tied to the underlying view framework. Perhaps this was an acute issue for me because I wanted to migrate between two different view frameworks. But I think a framework specific approach to routing is also a constraint that impacts others because there are cases where you might want to reuse significant amounts of logic between platforms (web, desktop, mobile).

In an abstract sense, you need routing in all of these contexts (desktop, mobile, web) and across different frameworks in each context. So an immediate benefit is to facilitate migration between frameworks and contexts. But in extricating the routing out of my Angular application, I started to recognize the degree to which much of my logic had found its way into the view in one form or another. These thoughts started to further take shape after a lunch with Chris Marinos. We talked about React, Redux, dispatching, double dispatching and loading view data and it really helped me to understand at least some of the complexities.

As I worked through these issues, I tried out several different approaches and tried to build them into libraries. Now that I’ve had a little time to think about these things, play around with them and (most importantly) apply them, I see some definite patterns emerging that I think are really useful in the context of Redux. This article focuses on those patterns in the context of routing.

Note I say Redux and not React. My goal here is to arrive at an application architecture that is devoid of a view and that can be fully tested simply by initializing the application, dispatching actions and checking state. I don’t want any dependencies on a specific rendering framework. Of course, this idea of keeping logic out of the view is hardly a new idea. Outside of React, logic-less templating engines like StringTemplate have been pushing this agenda for years. This is simply my account of the patterns I’ve tried to establish to reach this goal in my applications to promote testability and portability.

I think ‘ui-router’ had the right abstractions for handling routing. Their approach wasn’t overly view specific, but I didn’t see any easy way to completely decouple it from the view. So I’ve tried to include many of the same abstractions here. The key thing, for me, was the declarative style of their approach. I liked the ‘ui-router’ approach of declaring all of the routes in one place and then associating code with entering or leaving particular routes. This approach really resonated with me. But I wanted to do it in a way that was external to the UI but integrated with Redux. Along the way, I also wanted to incorporate some useful type constraints when working in a TypeScript context (although types are not central to the pattern).

Let’s start by looking at some code. Here are some sample route declarations:

import vada = require(‘vada’);// Route to the main menu
export const mainRoute = new tsr.RouteId<{}>(“main”);
// Route to a list of quests
export const questsRoute = new tsr.RouteId<{}>(“quests”);
// Route to a specific puzzle
export const playingRoute = new tsr.RouteId<QuestParams>(“playing”);

Essentially, what I’m doing here is declaring the various routes I’m interested in. In ‘ui-router’ parlance, these are called “states”. But in a Redux context “state” already has an accepted meaning. So I chose the term “route” although it is essential the same thing as what `ui-router` refers to as a state. The idea here is that these are different “modes” the application can be in. Each mode determines (via type annotations) what information is associated with each route. Switching between modes can impact the application state but we’ll come back to that in a second. Note that there are no URL patterns here. We don’t add that until we actually connect our application to a browser. For now, these are just first class representations of our routes.

Within the vada module (which is essentially code to implement this view agnostic routing approach as well as the Reactor and Operation patterns I talk about elsewhere), I also define a reducer specifically for handling route information and an action creator for manipulating the current route state. The route state is quite straightforward:

export interface RouteState {
name: string;
params: { };
}

If you aren’t familiar with TypeScript, this basically defines that my route state (which may exist somewhere within the global application state) should consist of a string name (for the route) and a parameters object. Using the action creator, one way I can then dispatch route changes as follows:

store.dispatch(vada.setRoute.request({
name: "playing",
params: {
quest: 1,
level: 2,
}
});

But this has me repeating the route name and the compiler has no way to check whether those parameters are the ones that apply for this route. As we’ll see in the next example, I can use those ‘RouteId’ objects I talked about earlier to ensure that I pass the correct parameters to each route and that I get the route name correct. This way, the only place the string representation of the route needs to appear is in the definition of the ‘RouteId’. Reusing the ‘RouteId’ objects avoids having to type the name repeatedly and potentially mistyping it. You may also have noticed the type annotations on the route definitions. These define the type for the ‘params’ part of the route state. This ensures that I always use the correct parameter names and associated types. So the equivalent and type safe way to dispatch a route is by using the ‘apply’ method on the ‘RouteId’ to construct the arguments to the ‘setRoute’ action creator, e.g.,

store.dispatch(vada.setRoute.request(playingRoute.apply({
quest: 1,
level: 2
}));

Note that the ‘playingRoute’ instance implicitly knows the route name it is associated with and the types of the arguments which are then statically typed checked.

The other main thing to deal with is “callbacks” that should be invoked whenever we enter or leave a given route. This highlights something that I think underscores the importance of a view agnostic approach. The route interacts with the application in two important ways. The obvious one is that it determines what to show. Making the view depend on the current route is straightforward with Redux and React.

The more subtle thing is how a change in route should impact the state of the application. The use case that really highlights this best for me is when I want to deep-link into a Single Page Application. At some point while using my application, A user might find their address bar says something like ‘http://example.com/#/customer/25’. I want them to be able to copy that link and send it to somebody and when that somebody opens the applications, all the information necessary to render that page needs to get loaded (ideally before I render). So the fragment ‘#/customer/25’ needs to trigger some code in the client. But my testing would be much simpler if I could test that loading without having to bring the view (browser) into it.

For this reason, I want to be able to (in as declarative a way as possible) specify that whenever I enter a given route some code is run to help initialize that view. Ideally, this initialization code is run synchronously before the state is even rendered in the view. If this initialization instead triggers asynchronous requests, the state should be altered to reflect that these requests have at least been made.

To implement this, I use Reactors. As a quick aside, a Reactor is a fragment of code that responds to changes in state. It is composed in such a way that it wraps an existing reducer function to produce a new, enhanced reducer function. The key aspect of a Reactor is that it is not part of the action, it is a response to specific changes that (potentially many) actions might make to the state. This allows us to create small, modular, declarative and composable pieces of code that can be integrated into our application.

A special case of a Reactor is one that specifically watches the route state and checks for changes. There are two important flavors of such Reactors. One that watches for cases where we enter a given route and one that watches for cases where we leave a given route. Both of these are just extensions of Reactors in general.

For example, let’s say our application is a game. One of the routes of the application is to a “main menu”. This main menu may feature two options “Start New Game” or “Load Level”. Both of these are essentially doing the same thing behind the scenes, they are leading us toward a view where we will actually be playing the game (vs. navigating the app).

If I want to load the data associated with the particular level they are sending us to, I don’t want to include that in the actions. The first reason why I wouldn’t want to do that is that I already have actions for sending me to a different route. I can simply use those actions and associate them with these buttons. I don’t need new “enhanced” versions of those actions because it would make them less modular and less reusable by doing that. I want to stick with my lightweight universal actions for switching routes.

But another reason I wouldn’t want to associate the initialization code with the actions is that there might be several actions that trigger the same initialization. So what I really want to do is recognize, from the state change, that something is “missing” in the state (given the current route/view) and trigger loading that. This makes the application more robust because the initialization is tied to the actual need for the information, not all the possible ways that the need might be brought about. This is a subtle point, but one worth thinking about.

So in the case of the game example, we know that if the user ever ends up at the “playing” route (i.e., actually playing the game), we need to have actual level information loaded. To accomplish this, we can create a simple Reactor like this one:

const loadPuzzle =
vada.onEnter(playingRoute, (s) => s.route, (s, p) => {
return LoadLevel.evaluate(s, p);
});

The first argument, ‘playingRoute’, is the route (again, no string literals, very DRY) and this Reactor will trigger when we enter that route. The second argument is a closure for extracting the route information from the overall application state. The last argument is a closure that performs the state transformation, if any, that should get executed after we enter the specified route.

Then we can take any existing reducers for our application and simply
wrap them, e.g.,

const rootReducer = vada.wrapReducer(appReducer, [loadPuzzle]);

This is essentially middleware around our reducer. It runs the Reactors in the order they appear in the array and performs any transformations that would be triggered. The essential point here is that there are no side-effects. This is just a reducer.

An alternative approach might have been to subscribe to the store. That approach was an inspiration for much of this work. But the issue there is that ‘subscribe’ is not as predictable. The subscriber may trigger a dispatch but when is that subscriber notified? And which subscribers get notified first? And which dispatched actions get triggered first? And what happens if the dispatched actions trigger state changes themselves? And what happens if actors trigger each other in an endless loop? With the inline wrapping of the reducer, all of these questions have clear and deterministic answers.

All of this has nothing to do with the view. The logic for what happens when we enter or leave each route is now completely outside the view and so is the overall routing as well. I suspect some people will think that such a separation is actually more complicated and unintuitive. Many routing approaches are, indeed, integrated into the view, e.g.,

<Router history={browserHistory}>
<Route path="/" component={App}>
<Route path="about" component={About}/>
<Route path="users" component={Users}>
<Route path="/user/:userId" component={User}/>
</Route>
<Route path="*" component={NoMatch}/>
</Route>
</Router>

I’m not arguing this is “wrong”, just that I prefer to try and keep as much application logic out of the view as possible.

So far, I’ve talked about how all this is built around reducers. But where is the connection to the browser? Tools that integrate routing with a view and the browser definitely have an advantage because the integration with the browser is already taken care of. In my case, what I did was to use Crossroads and Hasher to bind routes (as abstract concepts) to specific URLs in the browser. As is my preference, I tried to make this as declarative as possible, e.g.,

let main = bindRoute(mainRoute, "main");
let quests = bindRoute(questsRoute, "quests");
let playing = bindRoute(playingRoute, "playing/:quest:/:level:");
initializeRouting(vada.routingCallback(store, () => {
// What happens in the case of an unknown route…
main.goto(null);
}));

Again, I use the ‘RouteId’ object to avoid repeating myself and type information to provide some degree of type safety (although no type annotations are present, type information is being inferred in this example). These associations are essentially adding code that watches the ‘#’ fragment in ‘window.location’ for specific patterns. If it finds one of those patterns, the browser dispatches an action to change the route state.

Of course, a route change can also be triggered programmatically. For example, in the code above we have a callback that is triggered whenever we encounter an unknown route. In this case, we use the `goto` method on our “main” route to trigger a route change. In such cases, we want to make sure that the browser history is “in the loop”. If we simply changed the route in memory, that would be very confusing for the browser.

The `goto` method takes care of that. Instead of dispatching on the store directly (which would change the state out from under the browser’s nose), the ‘goto’ method sets ‘window.location’ and lets the code that monitors current location take care of dispatching to the store. Of course, once we bring the view into things, the easiest way to trigger a route change is to simply follow an ‘href’. The important point here is that we don’t need any Javascript code to fire when we click on a button. We just need the appropriate URL to send people to. In that case, an ordinary ‘<a>…</a>’ tag will do. Clicking the link causes a change in location and that sets the dispatching machinery in motion.

Let me summarize the sequence of events when the browser is in the picture. The user selects a URL (either by typing it in, clicking on it in an email or clicking a button in the UI). The application sees the URL, matches it to a pattern and dispatches an action on the application state. The reducer function is called. The underlying reducer is called (changing the route) and any Reactors are triggered as part of this. Finally, the reducer completes and the view is updated (once).

Of course, when the view is updated, we want to be able to have a nice declarative way of indicating what to show. We can still do this in much the same way as we would with a view-integrated router. But this can now do this much later in the application development process. I’ve created a couple of React components for this purpose that mimic the basic behavior of ‘react-router’, e.g.,

<Provider routeStore={routeStore} store={store}>
<Route route={mainRoute}>
<Main/>
</Route>
<Route route={questsRoute}>
<QuestMenu playing={playing}/>
</Route>
<Route route={playingRoute}>
<Puzzle/>
</Route>
</Provider>

But we aren’t specifying anything about the URL patterns or strings involved. These are effectively just conditional DOM elements which are show or hidden depending on whether the current route matches a provided ‘RouteId’.

In Practice

I’m still in the process of trying to migrate some existing Angular code over to this kind of approach although the routing part has been completed. Much of what I’ve talked about here was motivated by that effort to reproduce ‘ui-router’ capability outside of the view.

But I wanted to apply this idea to a greenfield project. So, over the holidays I implemented a puzzle game idea I had been working on and tried to build it according to this architectural approach. I’m quite pleased with the outcome. I was able to create a code base that handled the actual game and defined reducers for the actions in the game. But then layered the overall application (startup, menus, etc.) on top of that combining the game reducer with other reducers, like the route reducer, that defined the application flow. Included in the application flow were the Reactors which could be composed to define additional logic associated with transitions.

With this approach, I was able to write lots of tests around all the different aspects of how the application should work (e.g., specifically navigating the application from the main menu to each puzzle in the game, dispatching actions to solve each puzzle and then confirming that I had reached the proper end state). All of this was done without the need to bring in the browser, browser based testing, DOM manipulation or any kind of view. In fact, I wrote it all before I even started on the UI.

Once that was done, I was able to very easily create a view using React. This was the first time any view specific dependencies were introduced (in fact, it is a completely separate code base that has the application code described in the previous paragraph as a simple ‘npm’ dependency). Building the views was really easy. Most of the view work was to create a visual representation of the underlying application state. Some remaining work was required to translate touch gestures and mouse events into actual moves (game actions to be dispatched back to the store). But overall, it was possible to maintain a complete separation between the view and the application logic.

I followed an almost identical approach with an implementation of TodoMVC although instead of being separate modules, they are simply separate files. Again, I was able to test the application logic without using any of the view code.

I’m confident that if I wanted to create a native application of either of these, using something like React Native, I could use the core application code I’ve already written pretty much as is and simply add a custom view for that platform.

Conclusion

Lots of people are both familiar and happy with routing as integral to the UI. I’m not really making a judgement here about what is “right” or “wrong”. But for me, defining the route entirely outside of the view feels right and I’d say this decision has been confirmed by the ease with which I can write tests using simple tools like ‘mocha’ that exercise not just the route and state transitions, but the reactive bits of code that are responsible for loading view data. In this way, the entire test simply involves initializing a store, dispatching actions to it and checking state. Furthermore, the application logic is represented in a modular and composable way.

Keep in mind that a “route” is a very useful concept. It isn’t just about browsers. Routing is really a general concept that can be applied to desktop, web and native applications. By moving it out of the view and associating it more closely with Redux and application state, I think the overall architecture is much cleaner and more flexible, IMHO.

--

--