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 . That specifically means staying alert to your blind spots. staying conscious Our blind spots are the traps that lead us to taking longer less intuitive development paths when there are obvious better/faster ways that we aren’t seeing. within our capabilities 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 and . React Redux Today what I’m going to propose is the same thing with . Yes, you heard me. Or, just following the status quo, and paying the price for it. Routing The less sexy, supposedly solved problem that we’ve all been reinventing for years, from language to language, framework to framework. 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: redux-little-router 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 and compose custom middleware, reducer, and enhancer: (yes there are many , but few have caught on) 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: { 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: . It also prevents inconsistencies, resulting from different sets of state, in the props your components receive (a React Router problem). 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. 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 called My summary is: three part series “Let The URL Do The Talking.” 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 series. It’s a problem. Rather, there are lots of problems. It’s unfortunate because , but it makes little sense for Redux apps. “Let the URL Do The Talking” React Router is great 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 in your reducers. What can you do with that though when the type is always LOCATION_CHANGED? (reads: useful stuff) 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 />. Having the params is nice and all, but if we don’t know what they correspond to our reducers are crippled. But what if we could get the type to be something more relevant? 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 you have your users fetched + stored in Redux. and 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 . What we are talking about is a problem that redux-little-router carried over from the React Router days. predictable single source of truth 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? . The type needs to correspond to the URL. So the solution is as simple as can be: Actions Dispatching LOCATION_CHANGED as the type is the precise missed opportunity here. 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 and use connect. The address bar will be handled for you. flux standard actions 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 memoization const 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)(UserComponent) // could be a switch (but this is cached): const pages = { homeScene: HomeComponent, postScene: PostComponent, reposScene: ReposComponent, repoScene: RepoComponent, userScene: UserComponent } 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 apps should be handled. combination state-driven + url-driven “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. Which is why you have to wonder: It’s about getting rid of it. “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 your 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. So,push, replace, go, and all the related methods carried over from the package can now be removed. You could very easily use this pattern on an existing app without changing much. imperative history route-matching components like <Fragment />, as you know, can go too. annoying providers that nobody likes like <RouterProvider /> (besides Redux’s of course) 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 — . 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: another carry over from React Router 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 . It allows developers to use the same paradigm they’re used to (components) everywhere. in a world without Redux state 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 . This happens all the time in our projects when we try to apply 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. trying to fit a square peg in a round whole yet again (reads: “force”) 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: 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 45% of the time (using switch or hash) And the other of the time, the logic is usually the equivalent of an else branch with another binary or hash. 10% 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): url: /home/bio/dat-boi 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 feature 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 a better solution than , 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 benefit — by doing it without route-matching components, you take that a step farther. not state you can represent flatly in redux coupling-removal 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 , should I use variables for the path prop lol? in one place 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 now. I don’t see the value. a thing 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, c_onnecting 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 and interpolate some strings. You’re not [easily] doing that without action types. Period. (or “params”) 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 in the solution to the problem. core 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. They no longer exist. You can cross the need for nested routes off your list too. Poof. 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 , but they have done a disservice to it by undermining it — perhaps just with the word It subliminally conveys it’s not as powerful as route-matching components. But my perspective is that couldn’t be farther form the truth — . And reducers combined with tools like reselect offer far further caching + performance benefits. This brings us to another point. right conclusion “just.” for, your reducers are the most powerful of all 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/ doesn’t use Redux, React Router gets it right. There was an initial issue with older versions of React Router that the 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. site “Let The URL Do The Talking” TYPES, CHECK. BUT ISN’T ROUTING EASY? If you read the “Let The URL Do The Talking” series, you probably stumbled over the regarding how the Redux docs portray routing as “easy.” Both the creators of redux-little-router and I agree: . The difference is it’s not a centralized computer-scien_cey_ problem, but rather a never-ending laundry list of that must be done right. part this isn’t true related tasks Things such as , are time-consuming things developers shouldn’t have to do. Contrary to how easy the Redux docs initially portrayed it. scroll restoration, redirects, data-fetching, chunk pre-fetching , Android BackHandler on Native, the list goes on 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 or a single immutable state store + serializable actions + pure functions How many more times will great developers reimplement a router? That is the question. as in React as in Redux. 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 check off? Redux-First Router Well for one, you can when a corresponding action is dispatched. They also resolve on the server as part of SSR before you render your app. automatically resolve thunks 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 and , both on the server and the client. not found redirects 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: “ ” 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 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. dat chunks option tho! far 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: faceyspacey/redux-first-router