dCompanytech blog

Do you really need React Router?

By Gunnar A. Reinseth
Published 28 April 2025

Routing in a Single Page Application (SPA) doesn’t need to be a complicated affair. Make a routing table centrally in the app, wire it up to the browser history, and wire the matched route to its corresponding view.

React Router has chosen a different approach: route definitions are woven into the view components, and they can be defined anywhere in the component tree, making them indirect, hierarchical route definitions. The predecessor angular-ui-router demonstrated this concept more than a decade ago, and I was quite enthusiastic about it at first. I was working on a project at the time that had a page structure that looked something like the following:

+----------------------------------+
| MAIN MENU                        |
+----------------------------------+
| SUB MENU                         |
+------------+---------------------+
|            |                     |
| SIDE PANEL | MAIN PANEL          |
|            |                     |
+------------+---------------------+

With angular-ui-router it was pretty cool to be able to define a structure where the dependencies between the page modules and the data they required could be turned into a hierarchical, reactive machinery. Selecting an item in the main menu triggered the sub menu load, which in turn triggered the side panel to load, which finally triggered the main panel to be loaded and be presented. A cascade effect if you will. My colleague and I were happily observing the pieces falling into place one by one, spinners turning into data in a beautiful domino effect, until my colleague remarked “but it’s a bit slow, isn’t it?” Enthusiasm gone, we had to admit that this might not be such a good idea after all. All the data should load at the same time, not one after another in a sequence of events. So we put angular-ui-router aside and moved on.

React Router appeared a couple of years later and built on this concept, despite the obvious drawbacks. And another one is that it’s hard to get an overview of the route definitions since they’re scattered throughout. This, in turn, makes it challenging to be able to verify the route table with unit tests (e.g. to uncover duplicates or routes that cannot be reached, something that happens from time to time in large code bases with many developers).

It should be noted that you don’t have to use the hierarchical functionality of React Router. All routes can be defined as data at the top of the application. Or wait, can they? Yes, it seems so. Hold on, they can’t after all? React Router has had a hard time making up their minds, it seems (apparently routes can be data again), and this silly statement leads us to the next and perhaps most important problem with React Router: backward compatibility, or the lack of it.

React Router loves breaking changes. They have made fundamental changes in every major version, and they are currently on v7. They love it so much that they have even defined a concept for future flags that allows them to define future breakages (as they’re obviously coming). Maybe it’s their way of trying to be “nice” about it and give some heads up. And it might just be that the people behind React Router have been brought up in the prevailing “break often” culture in the frontend world and simply do as they have been taught. But I doubt that. These are really smart and good developers. The tools they create are de facto standard with thousands of downloads every single day. This makes them role models for other developers in the community, and it makes me a little sad that they do not make an effort to create a world where breaking changes are the exception, not the rule.

In summary

To be fair, the React Router people makes some good stuff. They support data-oriented routing again, and they have addressed the cascade problem by introducing the concept of data loading (it took them 10 years, though). But the underlying problem remains: mixing routing and presentation (and now also data loading) leads to increased complexity. Split the concerns, following the principles of Functional core, imperative shell, and the world becomes a lot easier to manage and maintain. Add few or no breaking changes to the mix, and life will be good.

So what should you be using instead? There are plenty of libraries out there. My advice is to find one that is purely data-oriented, and one that doesn’t have the habit of breaking the world all the time. Or maybe you should write the router yourself?

(to be continued)

frontendrouter
Powered by powerpack