Pre Release: Redux-First Router — A Step Beyond Redux-Little-Router
UPDATE (7/11/17): PLAY WITH THIS RIGHT NOW IN YOUR BROWSER:
The purpose of this article is to debunk the effectiveness of route-matching components + nested routes when using Redux, while discovering a better, simpler, obvious way.
As a conscious developer, we have to realize our #1 goal is in fact staying conscious. That specifically means staying alert to your blind spots.
Our blind spots are the traps that lead us to taking longer less intuitive development paths when there are obvious better/faster ways within our capabilities that we aren’t seeing.
As a result we are always looking for a faster more intuitive way to do things.
It’s no surprise every few months/years/days we find ourselves confronted by the fact that there’s a better way to do what we’re doing.
Sometimes these evolutions are so profound that it leads to the entire development community switching how they do things, as is the case both with React and Redux.
Today what I’m going to propose is the same thing with Routing. Yes, you heard me. The less sexy, supposedly solved problem that we’ve all been reinventing for years, from language to language, framework to framework. Or, just following the status quo, and paying the price for it.
REDUX-LITTLE-ROUTER
We’re going to start our journey through understanding the problem by looking at the best thing out when it comes to routing with Redux:
If you aren’t using Redux, React Router is still the b̶e̶s̶t̶ recommended solution. So, this article is strictly for Redux developers.
How does redux-little-router work?
Taking an example from its readme, you start out by defining and nesting routes like this:
const routes = {
'/users': {
title: 'Users'
},
'/users/:slug': {
title: 'User profile for:'
},
// nested route: '/home': {
title: 'Home',
'/repos': {
title: 'Repos',
'/:slug': {
title: 'Repo about:'
}
}
}
}
Then you do what’s typical of redux routers (yes there are many, but few have caught on) and compose custom middleware, reducer, and enhancer:
const { reducer, middleware, enhancer } = routerForBrowser()
const reducers = combineReducers({ router: reducer, ...others })
const enhancers = compose(enhancer, applyMiddleware(middleware)
const store = createStore(reducers, enhancers)
Lastly, you setup your provider(s):
import { Provider } from 'react-redux'
import { RouterProvider } from 'redux-little-router'
import configureStore from './configureStore'const store = configureStore()()ReactDOM.render(
<Provider store={store}>
<RouterProvider store={store}>
<YourAppComponent />
</RouterProvider>
</Provider>,
document.getElementById('root')
)
The primary ways to use it are:
Change the URL which results in a corresponding action dispatched:
push('/users/james-gillmore?foo=bar')
Conditionally render something using their “route-matching” component <Fragment />
(same thing as <Route />
in React Router):
<Fragment forRoute='/home/repos/:slug'>
<h1>This a code repo!</j1>
<ConnectedRepo />
</Fragment>
Create a real link for SEO:
<Link href='/home/repos/react-universal-component'>
Go To React Universal Component
</Link>
You can also nest fragments/routes like in React Router. Here’s an example from their readme:
<Fragment forRoute='/home'>
<div>
<h1>Home</h1>
<Fragment forRoute='/bio'>
<div>
<h2>Bios</h2>
<Fragment forRoute='/dat-boi'>
<div>
<h3>Dat Boi</h3>
<p>Something something whaddup</p>
</div>
</Fragment>
</div>
</Fragment>
</div>
</Fragment>
One thing to note about this example is there is a page called
'/home/bio'
that will show just<h2>Bios</h2>
.
So obviously the goal of that snippet is to show a page for the URL /home/bio/dat-boi
. Similar to what you can do with React Router.
Before we continue, it’s important to look at the actions dispatched that allow for this:
{
type: 'LOCATION_CHANGED', // the same for all actions
pathname: '/home/repos/react-universal-component',
route: '/home/repos/:slug',
params: {
slug: 'react-universal-component'
},
query: {
foo: 'bar'
},
search: '?foo=bar',
result: {
title: 'Repos about:'
parent: {
title: 'Repos',
parent: {
title: 'Home'
}
}
}
}
Lastly, here’s your “location” state stored in your router
state key (which is used to power Fragments):
{
pathname: '/home/repos/react-universal-component',
route: '/home/repos/:slug',
params: {
slug: 'react-universal-component'
},
query: {
foo: 'bar'
},
search: '?foo=bar',
result: {
title: 'Repos about:'
parent: {
title: 'Repos',
parent: {
title: 'Home'
}
}
}
previous: {
pathname: '/home/repos',
route: '/home/repos',
params: {},
query: {},
result: {
title: 'Repos about:'
parent: {
title: 'Repos',
}
}
}
}
The state is a replica of the most recent action and the previous action
Before we move on to other ways to do this, it’s important to insure everyone is up to speed.
Basically instead of React Router having a separate state store, Redux state powers the <Fragment />
components. They are connected to state via react-redux’s connect
method. Well, the context provider at the top level of your app is, which is also an issue we’ll address today.
This allows for ALL the standard Redux patterns and tools you may use: reselect, async middleware that needs to know URL state, etc. It allows for you to make decisions in one place (your reducers/selectors) with both kinds of state. It also prevents inconsistencies, resulting from different sets of state, in the props your components receive (a React Router problem).
I won’t go much more into how redux-little-router was a level-up from React Router. They came out guns blazing explaining the problems of using React Router with Redux in their three part series called “Let The URL Do The Talking.” My summary is:
Combining state from both React Router + Redux can be unpredictable, couples your view layer to state, and complicates producing refined state from the combination of URL state + Redux state (and in many cases, makes it impossible).
If you’re using React Router with Redux you really need an alternate Redux-first solution. If you don’t believe me, again, make sure you read the “Let the URL Do The Talking” series. It’s a problem. Rather, there are lots of problems. It’s unfortunate because React Router is great, but it makes little sense for Redux apps.
So going forward, we’re going to assume we’re in agreement, and generally like what you’re seeing with redux-little-router.
So, What’s The Problem With redux-little-router?
Or rather, what can we do better?
For one, it means we repeat ourselves when we constantly tap address bar paths in components (DRY). Secondly, if you change your apps URLS/paths, you have to change them everywhere in code. But more importantly, it means your littering your codebase/component_tree with lots of additional nesting.
Paths are also subject to change, whereas it’s your job to use well-named variables (i.e. state keys) that will stick around for a long time. The bulk of your codebase would be better off comprised of the symbols + variables you carefully can choose and change (not what you or your client chose long ago).
That’s just the beginning though, and not the driving factor here. I just wanted to get those 3 things out of the way.
WHAT THEY GET RIGHT
What redux-little-router gets very right, though, is the bi-directional transformation of actions to URLs and URLs to actions (which by the way is embodied by the blue two-way arrow in the graphic at the top of the page). What they also get right is how rich your actions are in information. Specifically, they have all dynamic segments from your paths transformed to params. You can use your paths + those params to do some real damage (reads: useful stuff) in your reducers. What can you do with that though when the type is always LOCATION_CHANGED
?
The type unfortunately is basically useless, as it’s going to be LOCATION_CHANGED all the time.
As a result you still must depend on <Fragment />
.
But what if we could get the type to be something more relevant? Having the params is nice and all, but if we don’t know what they correspond to our reducers are crippled.
Well, we could write lots of reducer code to analyze what those paths mean (and expend the computation cycles to execute it over and over). I don’t think anybody does this, or at least not nearly enough of it.
INTERLUDE
As far as Redux developers are concerned, using <Route/>
or <Fragment/>
is a carry-over from the bygone era of React Router. Route-matching components make a whole lot of sense when you have nothing else to make decisions based on. But you do.
Conditionally rendering based on both kinds of state (URL + regular Redux state) is still not solved by <Fragment/>
. For example: if you want to show something if the URL is /users
and you have your users fetched + stored in Redux.
And it’s not very informational compared to the infinite custom logic you can come up with in your reducers.
The realization is that — if used to the fullest, which we’ll get to soon — a global reactive state tree completely replaces the need for route-matching components like <Route/>
in React Router or <Fragment/>
in redux-little-router. Read that again.
Your redux state completely wipes out the need for route matching components. Pick one or the other, you don’t need both. And keep in mind, we’re not talking about the initial problem redux-little-router solved regarding having a predictable single source of truth. What we are talking about is a problem that redux-little-router carried over from the React Router days.
Using
<Route />
or<Fragment />
is a carry-over from the bygone era of React Router.
INTELLIGENT ACTION TYPES ARE THE SOLUTION
So how do we make our state informational and effective enough to go without route-matching components. Well, where do they derive their values from? Actions. Dispatching LOCATION_CHANGED
as the type is the precise missed opportunity here. The type needs to correspond to the URL. So the solution is as simple as can be:
const routesMap = {
REPO: '/home/repos/:slug'
USER: '/users/:slug,
POST: '/posts/:id',
HOME: '/'
}
WE ASSIGN A TYPE TO EACH PATH. No matter what the values are for :slug
in the example, the corresponding action receives the type its paired with. Story over. Pass go. Collect $200.
WHAT ABOUT THE URL + ADDRESS BAR??????????? “JUST DISPATCH ACTIONS!”
Now you can dispatch actions like this:
dispatch({ type: 'REPO', payload: { slug: 'redux-first-router' } })
dispatch({ type: 'USER', payload: { slug: 'james-gillmore' } })
dispatch({ type: 'POST', payload: { id: 123 } })
dispatch({ type: 'HOME' })
Once you setup your
routesMap
and configure your store, there is virtually zero you can do, which is a good thing. There is no API surface. Just dispatch flux standard actions and useconnect
. The address bar will be handled for you.
Your links can look like this:
<Link href='/users/james-gillmore' />
<Link href={{ type: 'POST', payload: { id: 123 } }} /> //best option
<Link href={['users', slug]} /> // same as /users/james-gillmore// sneak-peak using react-universal-component + webpack-flush-chunks
<Link prefetch href={{ type: 'POST', payload: { id: 123 } }} />const slug = getSlugFromSomewhere()
Using actions as the
href
allows your paths to be defined in a single place!
And now your reducers can look like the following. Let’s take the 'USER'
action type as an example:
const userSlug = (state, action) =>
action.type === 'USER' ? action.payload.slug : state// we also need the following (not all types need to have URLs):const usersCache = (state, action) =>
action.type === 'USERS_LOADED' ? action.payload.users : state
And lets connect a component:
const User = ({ user }) =>
<div>{user.name}</div>// in a real app you'd obviously use reselect for memoizationconst mapState = ({ userSlug: slug, usersCacher: users }) => ({
user: users[slug]
})export default connect(mapState)(User)
Which we can use like this:
const App = () =>
<div>
<UserComponent />
</div>
Let that sink in.
Here’s another kind of reducer you’d likely have:
const page = (state, action) => {
switch(action.type) {
case 'HOME':
return 'homeScene'
case 'POST':
return 'postScene'
case 'REPOS':
return 'reposScene'
case 'REPO':
return 'repoScene'
case 'USER':
return 'userScene'
}
return state
}
And its corresponding component:
const PageComponent = ({ page }) => {
const Page = pages[page]
return Page ? <Page /> : null
}const mapState = ({ page }) => ({ page })
export default connect(mapState)(PageComponent)// could be a switch (but this is cached):const pages = {
homeScene: HomeComponent,
postScene: PostComponent,
reposScene: ReposComponent,
repoScene: RepoComponent,
userScene: UserComponent
}
And the <PageComponent />
‘s usage:
const App = () =>
<div>
<Header />
<PageComponent /> // no props--because it's a connected
<Footer />
</div>
NOTE: Using
<PageComponent />
is similar to stacking<Route />
components that each point to a component, with the main difference being that you manually (and more flexibly) use redux state to make the determination.
Take a second to grok that. Get a cup of coffee. Do what you gotta do. Let me know in the comments if you don’t agree or if this has been done already. I’m not referring to the “switch” or hash — I’m reffering to URLs paired with types.
From my perspective, it’s painfully obvious that action types corresponding to paths is how combination state-driven + url-driven apps should be handled.
“This is one of those things so simple I can’t believe I overlooked it. The solution is a perfect impedance match between the problem and the environment where it needs to be solved.”
It’s about keeping routing state out of the View layer. It’s even about keeping it out of your reducers. It’s about getting rid of it. Which is why you have to wonder: “why directly use any information about paths and incoming URLs, queries, etc, in your components?”
THE MOST MINIMAL ROUTING API SURFACE
With route-matching components off the table, let’s take a look at all we can strip from the already “little” redux-little-router:
push(‘/users/james-gillmore’)
. This changes the URL which results in a corresponding action action being dispatched. Conversely, Redux-First router lets you dispatch flux standard actions, and its middleware parses them and converts them to a URL that can be pushed on to the address bar. Essentially, the opposite of redux-little-router. By the way, applying this to an existing Redux app is insanely easy — just a matter of matching URLs to your main types. …So,push
,replace
,go
, and all the related imperative methods carried over from the history package can now be removed.- route-matching components like
<Fragment />
, as you know, can go too. - annoying providers that nobody likes (besides Redux’s of course) like
<RouterProvider />
query
+search
. You don’t need those since apps that take SEO seriously use paths. If you really need those, save those for async actions you dispatch (but you also don’t need them there).- nested routes???
Nested routes are arguably the most complex feature of redux-little-router — another carry over from React Router. So we can also get rid of nested routes, though it has a few more implications. Before we discuss how we can get rid of them, a history lesson is in order:
HISTORY LESSON
What happened is React Router came out and they had the novel idea that “everything is a component” — even routes and routing. That’s extremely useful in a world without Redux state. It allows developers to use the same paradigm they’re used to (components) everywhere.
Where, in my opinion, things went wrong is where Redux developers tried to apply the same concept where it wasn’t needed. It amounts to trying to fit a square peg in a round whole yet again. This happens all the time in our projects when we try to apply (reads: “force”) all that we have built (aka: our hard work and previous knowledge) to a new scenario, but where it’s the wrong tool for the job.
I say “yet again” because the “Let The URL Do The Talking” series uses the same metaphor for how they evolved the platform to this point.
Both React Router and little-redux-router force upon you the concept of “nested routes.” They convince you that you need them.
It’s imposing a design decision on your app without you even knowing it. Instead, how about this: if the state is X, render Y, if the state is A, render B.
React Router famously declares the following in their documentation:
Have you ever noticed your app is just a series of boxes inside boxes inside boxes? Have you also noticed your URLs tend to be coupled to that nesting?
All the route nesting that resulted never sat well for me. For 4 reasons:
- 45% of the time the decision is binary: show the sidebar or don’t:
showSidebar ? <Sidebar /> : null
- 45% of the time it’s pick one scene from a set (using switch or hash)
- And the other 10% of the time, the logic is usually the equivalent of an
else
branch with another binary or hash. - Lastly, how often do you have a 3+ levels deep app where at each level path segments add an additional view to the page?
I want to touch on the concept in the last bullet point.
Let’s take an example from redux-little-router at the top of the article (and their readme):
How often do you render <h2>Bios</h2>
with nothing on it?
If you can’t come up with a good example, that’s not a good sign. I know what use-case they are implying though — a better example is /posts
which shows a posts list and /posts/:slug
which shows the post profile.
Nested route-matching components is not a better solution than state you can represent flatly in redux, and which you can freely and flexibly tap into in an ad-hoc style (via connect
). By doing so, it removes coupling from your view layer to URLs. redux-little-router heralded this coupling-removal benefit — by doing it without route-matching components, you take that a step farther.
But, truly, is there any case (when using Redux) where this is useful? React Router has the following in their docs:
Yes, there are boxes within boxes. So what? Let’s break this down:
- because we are nested within the
'/'
path we gain the benefit of knowing to display the nav bar (binary decision) - because we have navigated to
'/repos'
we learn we need to display a sidebar (binary decision) - because we go to
'/about'
we figure out what goes in the primary box (switch or hash)
Here’s what the routes look like by the way:
<Route path="/" component={App}>
<Route path="/repos" component={Repos}/>
<Route path="/about" component={About}/>
</Route>
So what? You’re seriously telling me the only or best way to build my app is to be aware of the URL wherever I go? To be able to change this in the future in one place, should I use variables for the path
prop lol?
Note: this isn’t just a “straw man” example easy to take down. If you break your app down into small connected components as you’re supposed to with Redux, this is what it looks like the vast majority of the time within each component.
Is this how you build native apps when/if you didn’t care about URLs? Is this truly the best design you would undertake if you didn’t have to deal with URLs?
It’s an unnecessary concept. In addition, besides the sidebar and navbar, in most apps each URL leads to a differently designed page. Sure, maybe because React Router promotes this user experience (where you have a page just containing <h2>Bios</h2>
), there are more apps like this than not. I.e. maybe it’s a thing now. I don’t see the value.
BYE BYE NESTED ROUTES
Route-matching components are geared towards a nested environment, such as your component tree. However if you’re doing things right, connecting Redux state to your component tree is a flat experience as far as each individual component is concerned.
Therefore if we got rid of route-matching components, we can also get rid of redux-little-router’s nesting feature in their routes map (which arguably is their most complex feature).
However it relies on the premise that our actions have informative types. Without informative types, your reducers can’t make full use of actions dispatched to you. As a result, with redux-little-router, you need to have nested routes in your routes map to fill in missing information.
Do you really need to indicate the title of something in a route map when you can do that when your reducer knows the type? What if you want those titles to be dynamic ? Static strings, as redux-little-router only seems to support, is great and all, but I rather have the ability to combine dynamic values in a payload (or “params”) and interpolate some strings. You’re not [easily] doing that without action types. Period.
In short, nested routes (within their route map) is useless. In my opinion it’s also done a major disservice to the adoption of what otherwise has been the closest thing to the true version of React Router for Redux. This is primarily because nested routes are the first thing you see in the readme. It’s complicated. It scares people away. The first thing users need to see — in my opinion — is the simplest most powerful thing you can do that defines your package. It’s not a defining feature in my opinion, but like <Fragment/>
it confuses users into thinking its somehow core in the solution to the problem.
In summary, the Nested routing feature exists because the <Fragment/>
feature exists. They are solutions to each other. Without one, they cancel each other out. Poof. They no longer exist. You can cross the need for nested routes off your list too.
ASIDE: IT’S JUST A “SWITCH”
People often say “it’s just a switch” as the answer to routing with Redux. But the truth is routing comes with so many more related/coupled responsibilities.
When I hear people say “it’s just a switch,” I feel like it doesn’t get to the essence. I think developers who have come to this conclusion have come to the right conclusion, but they have done a disservice to it by undermining it — perhaps just with the word “just.” It subliminally conveys it’s not as powerful as route-matching components. But my perspective is that couldn’t be farther form the truth — for, your reducers are the most powerful of all. And reducers combined with tools like reselect offer far further caching + performance benefits. This brings us to another point.
Like React Router, redux-little-router re-renders the virtual DOM on every location change in its top-level provider. react-redux jumps through so many hoops to re-render at the component-level for one reason: it’s more performant. It only re-renders leaf nodes. It’s so important they have you in user-land also jump through hoops by all the mapStateForProps
functions you must write.
To be clear in all of this, everything I’ve described only applies to a world with Redux. If your app/site doesn’t use Redux, React Router gets it right. There was an initial issue with older versions of React Router that the “Let The URL Do The Talking” series pointed out: you can’t pass props to route components. React Router has since since solved that in 4.0 by providing render
and children
props which takes an inline function. Redux-First Router is to Redux, what React Router is to React.
TYPES, CHECK. BUT ISN’T ROUTING EASY?
If you read the “Let The URL Do The Talking” series, you probably stumbled over the part regarding how the Redux docs portray routing as “easy.” Both the creators of redux-little-router and I agree: this isn’t true. The difference is it’s not a centralized computer-sciencey problem, but rather a never-ending laundry list of related tasks that must be done right.
Things such as scroll restoration, redirects, data-fetching, chunk pre-fetching, Android BackHandler on Native, the list goes on, are time-consuming things developers shouldn’t have to do. Contrary to how easy the Redux docs initially portrayed it.
If React comes in at #1 in importance and Redux at #2, Routing is close behind at #3. It’s why React Router is so popular and has played a central role in so many apps.
It’s funny that it has taken longer to click than declarative UI + uni-directional data-flow as in React or a single immutable state store + serializable actions + pure functions as in Redux. How many more times will great developers reimplement a router? That is the question.
A POWERFUL REDUX-FIRST ROUTER
So what would make a Redux-first router powerful? What features in the never-ending list of router-related tasks does Redux-First Router check off?
Well for one, you can automatically resolve thunks when a corresponding action is dispatched. They also resolve on the server as part of SSR before you render your app.
const thunk = async (dispatch, getState) => {
const { slug } = getState().location.payload
const data = await fetch(`/api/user/${slug}`)
const user = await data.json()
const action = { type: 'USER_FOUND', payload: { user } }
dispatch(action)
}const routesMap = {
USER: { path: '/user/:slug', thunk }
}
Similarly there’s an idiomatic way to deal with routes not found and redirects, both on the server and the client.
It works well with Apollo, in which case you likely won’t need the thunk feature, but it’s there if you need it.
It has first class support for React Native, its Linking API, and the Android BackHandler.
It has scroll restoration plugins, both for web and React Native. They are plugins so as not to bloat the web-build if you don’t need them.
It also has first-class support for React Navigation (another plugin). This happens to be what I’ve been working on recently. It’s done, but still needs tests. There’s a boilerplate you can check out for that too! I won’t get into this as it’s definitely the subject for another post, basically what I have for React Navigation similarly fulfills a real need. I’m really excited about it. I mean, I think what I have there is similarly ground-breaking, but as with this I’ll let you be the judge of that :)
CONCLUSION
With the core realization out of the way, I’m going to close by listing the vast number of capabilities Redux-First Router has. Covering them is for another time.
Redux-First Router Capabilities List:
- Server-Side Rendering
- Scroll Restoration
- Redirects + 404s
- React Native
- First-Class React Navigation Support 🔮
- History entries state
- Imperative API based on the History package
- Automatic data-fetching via “route thunks” (we’ll cover this in depth along with SSR in the future)
- React Native BackHandler support
- React Native Linking API support
- mandatory
<Link />
component for SEO - Transition change callbacks
- Automatic Title management
- Automatic Back/Next detection
- Code-splitting/prefetching, a la React Universal Component + Webpack Flush Chunks 🎉
PS.
If you’re wondering how <Link prefetch />
works, it works like this:
const routesMap: {
HOME: '/',
POST: '/posts/:id',
USER: { path: '/users/:slug', chunks: [import('User')] }
}
Yes, routes can be objects, not just strings. There is obviously several route options available to you. For example, there is fromPath
and toPath
functions you can provide to bi-directionally transform your paths and dynamic segment values. See the docs to learn about all options available.
But more importantly: “dat chunks option tho!” Yup, prefetching dynamic imports isn’t just for Next.js. And you guessed it, there’s some coupling between Redux First Router and React Universal Component. Truth is I’ve worked far far longer on this (it was put into production first 8 months ago). But I wanted to present it along with prefetching and code-splitting to really make things pop. You know what I’m saying.
Give this a spin & feel free to comment. One love [js]. Peace out!
Tweets and other love is much appreciated!
…Oh, and here’s the link:
> For more idiomatic javascript in Reactlandia, read:
Tweets and other love are much appreciated. Find me on twitter @faceyspacey Want to stay current in Reactlandia? Tap/click “FOLLOW” next to the FaceySpacey publication to receive weekly Medium “Letters” via email 👇🏽