React-Router is the most popular routing solution out there for React applications – so much so, that a lot of developers think of it as the 'Official Routing Solution' for React. While I'm sure React-Router works quite well for a lot of people, I've always disliked working with React-Router. I'll try to summarize my reasons in this article.
React-Router is arguably the most popular routing solution for writing React applications today. Most frontend developers these days automatically include Redux, React-Redux and React-Router as de-facto dependencies for pretty much any application. Including Redux, is something I can understand, as it provides quite a few tangible benefits, but what I don't understand is why anybody would settle for a routing solution that is both inferior and needlessly complicated.
For a bit of background, let me say that while I've been working across the stack for the past 12 years, my primary preference has always been to work on the backend, or on non-browser front ends. I used to absolutely loathe writing front-end code for browsers, because it was too messy, and inevitably devolved into a huge mess of spaghetti code. Thankfully, the last few years, tooling for the front-end has improved tremendously and most of my pain points have been resolved. I started actively writing browser front-end code, when I first started learning React, some three+ years ago.
So as a guy who has done a lot of backend work, on both APIs and web apps returning rendered HTML pages, using everything from php running on cgi-bin (with paths to files mapped as urls), to Express and Koa, with well-defined explicit routing. I've got a very specific idea of what a router is, what it should do, and how it should do it. To me, a
Router is a piece of glue code that uses the requested url to identify a
Controller, and execute that controller, providing it with a certain
context, which usually contains information about the incoming request. That's it. Nothing more.
If you look into Express.JS's router, you'll see that is exactly what it does. It exposes an API, that allows you to register controllers to routes (and methods), and then when a request is received for any of the registered routes, it executes the registered controllers in the order they were registered in. It exposes a very simple API that nevertheless packs in a lot of flexibility and functionality into it. This is my gold standard for a router. It does one thing, it does it well, and it stays out of your way at all other times.
So let's jump into what I don't like about React-Router and why.
Everything is a component in React-Router.
My view of components has always been that they are part of the view layer, and when I write applications, I work to keep business logic like API calls, validation, etc. out of the actual component, and instead try to keep them at the controller level.
However, React-Router's 'everything is a component' approach to routing makes this impossible. Say I want to fetch some data from my backend before rendering a component, the solution to this in React-Router land is to create a new custom component which will 'block' the navigation until the data is made available.
According to React-Router docs, there are three types of components in React-Router, only one type of which (
Link) is actually a view component. The rest are there to enable routing. Why is my router play acting as React components?
Different Routers for Different Environments
React-Router exposes two different routers,
StaticRouter. If you need to make a few API requests to fetch data before rendering, then the way to achieve this differs significantly based on which router you're using. Pretty much all examples I've seen for server side rendering with React-Router, show significant differences in the server routing code, and in the browser routing code. This limits the amount of code that could possibly be reused between the server and the client.
On the server side, you actually have to use
matchPath an internal method used by the router, to manually check if the path matches the path of the incoming request, and then call the associated helper function to make the API call and get the data. Hang on a minute! Isn't matching a path and calling the registered controller literally the definition of a router? So to perform server side rendering with async data fetching using React-Router, you have to write your own mini-router inside the
Lock-in to Router Specific View Components
If you want React-Router to handle a route change, that is triggered by clicking on a link, in your application, you will have to use one of either
NavLink components exported by React-Router. If you don't use these components, React-Router will not catch the route change, and your browser will instead perform a full page redirect to the link url.
So you're going to end up with a lot of
NavLinks littered throughout your app. This means that, if you eventually decide to switch out of React-Router to something else, you'll have to rewrite every single component of yours that uses these components. I'm not a big fan of this lock in, and would prefer to do without it.
Integration with React-Redux
Under some very specific conditions, React-Router will actually not render updated components, when the url changes. Now to be fair, this is a very well known and documented situation, but the fact is that this is not really an edge case, and the probability that you'll encounter this in your app is quite large. And the way to fix it, is to add yet more boilerplate code.
And if you want to use Redux to manage your routes, that's against recommended practice. Instead you'll have to use a completely seperate
Connected React Router module to 'synchronize your routes with your store'. I don't even know what that means, or why I should want to do it.
Major Breaking Changes
This is my final reason for not using React-Router. React-Router v4 completely removed the concept of centralized route configuration, and they did not provide a fully functional replacement. As of today (4th of January, 2019), the
react-router-config package which was supposed to provide this functionality, is still in beta.
I understand that a project might want to make breaking changes, and in a lot of cases such breaking changes are completely justifiable, but when a large subset of your users are actively using a feature, and you decide to remove it, you should at the very least provide a stable replacement solution. I don't want to have to worry about a hypothetical future scenario when the developers decide to deprecate some feature that my app relies on heavily, and force me into a situation where I either have to rewrite a lot of things to remove dependency on that feature, or run an outdated version of the library.
So these are the primary reasons I dislike, and do not use React-Router. If you're interested in finding out what my router of choice for React applications is, then I'm sorry to disappoint you, but I don't really have one. Instead I just use a homegrown router that I've been using for pretty much all of my SPAs (both React and non React). It mimics the ExpressJS router's API, with a few tweaks so it can be used for isomorphic routing and rendering React components. I'll talk about how I handle routing in my React applications using this homegrown router in a future post.
I'd love to hear your opinion on things I've written in this post. Unfortunately, I've not yet found a nice lightweight comments widget to add on to my blog, so until then the only way I can take comments is via email. Do write to me at asleepysamurai at google's mail service.