What I dislike about react-router


Recently I was asked: “Why did I give up the react-router library and switch to my bike?” To be honest, I have been asked questions related to my version of routing five times already. The last time it was half a year ago, and since then I have a little forgotten the main reasons for my choice. Therefore, I decided to remember them and write an article about why react-router is not suitable for large projects, share my vision of routing and get criticism of my ideas from the wider community.

So let’s go

Today react-router is, one might say, a monopoly in the world of React. If a project needs routing, then most likely the choice will fall on this particular library. At least, this was the case in all projects where I participated.

But every time I got some nuances in my eyes. Basically, these nuances relate to the 5th version, but in the 6th there is something to complain about.

The saddest one that ruins my minimalist views

Let’s pay attention to the component.

<Route path="/" element={<Dashboard />}>

On the one hand, this is the main killer feature of the library. A person working with a react sees a familiar interface here in the form of JSX. This captivates many.

But such a simple notation works only in examples or in very simple applications.

And in the projects where I participated, a wrapper was always written over the components, which dynamically generated a set of these same elements from the config object. Maybe this is a coincidence, but it seems to me that this is the very specificity of routing. Such things cannot be described with jsx. This config object is usually assembled by going through several processing functions: part of the data at the front, part of it arrives from the server, some routes are cut off by access rights or simply temporarily disabled, in some, on the contrary, some common feature is added in the loop.

We get the following stages of creating routing:

  1. Creation of a single object with route configuration

  2. Recursive pass of this object to generate a set of react components in jsx

And now, attention. Guess what the react-router library does after this jsx is rendered? That’s right, again:

  1. Creation of a single internal object with route configuration.

proof:

I hope you understand my sadness.

Let’s admit to ourselves. The component does not render anything, props are simply taken from it and put into a single configuration object. It is unnecessary to insert it into jsx, it is enough to immediately transfer this data as a parameter directly to the main component:

<Router config={config} /> 

Then there will be only one stage (the one that is the first). That is, in the routing library it is enough to simply describe the config interface and that’s it.

Fortunately, the sixth version already has a useRoutes hook, into which you can transfer the config right away. But a light version would be enough for me, where I would transfer the config directly to , and not through the context, and without these legacy components like .

Most limiting. Lack of modularity

If you can close your eyes to the first drawback, then because of the second, I believe, it is impossible to build a large application from independent modules. And the point is this. The sample pages are simple. There are conditionally 5 routes and a couple of places where there is a transition between these routes. As long as the application is small, we can easily keep in mind the links to all the pages. But what if there are 50 pages? Or 150?

Imagine a large project that is divided into modules. One of the modules implements a reference book of books. It has a page with a list of books and a page with information about the selected book.

/books

/book/{id}

First, we decided to make the “books” module a submodule of the “store” module. Therefore, the link to the book will look like this:

shop/{shopId}/book/{bookId}

Within a month, these links were scattered across the rest of the modules: “authors”, “publishers”, “book rating”.

A month later, I needed to add a parameter to the link. For example, the language of a book. The book link should now look like this:

shop / {shopId} / book /{bookLang}/ {bookId}.

In principle, the problem is solvable, but very time-consuming, and leading to possible bugs if at least one link in any of the modules is not taken into account. And this is not just a search / replacement of a string – most likely functions were written to generate links by parameters. Finding such places is not so easy. The task becomes as difficult as possible if the “book” module is supplied as a separate npm module. It is necessary to notify the developers of all projects so that they correct all references to the “books” module. Everyone will be very “happy” to drop their scheduled tasks to change links.

To avoid such complications, the module must provide functions that will generate links to its pages. The functions must be backward compatible, but the generated links can change from version to version. Then in other modules nothing will need to be redone and nothing will break (Yes, I’m talking about SPA, which is not indexed by search engines, and links can indeed change painlessly in new versions of the application)

That is, in the 21st version we had the following functions for generating links:

const getBooksLink = () => "/books";
const getBookLink = (id) => "/book/${id}";

And in the 22nd they were corrected:

const getBooksLink = (lang = "en") => `/books/${lang}`
const getBookLink = (id, lang = "en") => `/book/${lang}/${id}`

The functionality of the book module has increased by one parameter, while the function interface remains backward compatible and the rest of the code remains unchanged.

But another problem arises. A module can generate a link only relative to its root. He doesn’t know the absolute path. After all, we can place the “books” module in the store module (shop / 23 / books / en / 123) or in the dashboard module (dashboard / books / en / 123). How can a module find out the root directory for its links?

One of the options – you can pass the root directory as a parameter.

But I would prefer it to be handled by the routing library. We pass in the config which module is inside which one. The routing library can compute an absolute path from a relative path.

For example, the config of the routes in the “books” module:

const booksConfig = {
	index: "books/en"
	children: {
		books: { link: getBooksLink, layout: booksLayout }
		book: { link: getBookLink, layout: bookLayout }
	}
}

insert the config of the “book” module into the config of the “dashboard”:

const config = {
   index: "dashboard",
   children: {
       "dashboard": {
           layout: dashboardLayout,
           children: {
	            books: booksConfig
           }
      }
   }
}

These examples are schematic to understand the point. The idea is that by pulling getBooksLink () from anywhere in the project, an absolute link relative to the dashboard should be generated.

If we once transfer the config to the “store” module,

const config = {
   index: "shop",
   children: {
       "shop": {
           layout: shopLayout,
           children: {
	            books: booksConfig
           }
      }
   }
}

then all links will be automatically generated relative to the store module. That is, it is enough to make changes only in two files: delete from one config, add to the other.

Unfortunately, this is not available in react-router.

The most insignificant. Unnecessary re-creation of components.

Let’s consider an example:

const Home = () => <><Header title="Home"/><div>home content</div></>
const About = () => <><Header title="About"/><div>about content</div></>
.........
<Route path="/home" element={<Home />}>
<Route path="/about" element={<About />}>

Do you think that

will be recreated if we switch from the / home route to / about and back?

It can be seen with the naked eye that yes, tk. and are two different components, they will be re-created each time together with the inner

component. But for
, only one title prop changes – completely re-creating it is clearly overkill. After all, the Header component itself can be very complex – for example, I met a Header, inside which there was a button with a full-fledged chat. Next to the header there can be a footer, a sidebar menu.

In the examples of the “react-router” library, this was solved very simply: the header, footer and menu are created outside the router, and the router only switches the content.

And then figure it out, as they say, for yourself. Insert if-chips: if this is not a dashboard, then we render the head; if it is a dashboard, then we do not render the head. But, excuse me, why do I need a router then, if I will render components manually by checking the current location.

And now how it should be in my opinion. Pages should not be components, but render functions.

const homeLayout = () => <><Header title="Home"/><div>home content</div></>
const aboutLayout = () => <><Header title="About"/><div>about content</div></>

Now try creating a component for an experiment:

const Pages = ({page}) => {
	const layout = page==="about" ? aboutLayout : homeLayout;
	return layout();
}

Will you have to recreate the

component? Of course not, because only the render function changes, not the component. Only different titles will be passed to props, but the component will not be recreated in any way.

Why they didn’t do that in the library is a mystery to me. Moreover, in the fifth version there was a render parameter, but it still recreated the internal components. Only one thing comes to mind – apparently for the majority this is really insignificant. But for me this was the last straw, after which I wrote my router.

My quirks. Division into business logic and UI.

If suddenly you decide (like me) to separate the business logic from the UI, then I think it would be logical for the UI (which is in JSX) to only pull the “X button pressed” event, and the business logic did the transition itself. That is, the UI should answer the question “what did the user do?” but for “what to do with the program?” business logic must be in line.

Thus, the component does not make sense in this approach.

Instead, we have a regular button or tag:

<button value="user1" onClick={props.onOpenUser} />

and the event handler:

const onOpenUsers =
		({target: {value: userId}}) => history.push(users.getUserLink(userId))

Yes, I realize that in this approach the built-in functionality of the browser “open in a new window” will stop working, it will have to be implemented separately, but everything has its pros and cons.

conclusions

Most likely, different projects require different routing implementations.

In my projects, I will use my own implementation (unless, of course, you convince me directly here).

It is more functional and fixes all the shortcomings described above: it helps to make absolute links from relative links using the current config tree and does not re-create internal components again.

At the same time, it is more stripped down (only about 100-200 lines of code): nothing superfluous, relatively speaking, there is only one component and that’s it. In fact, the fact that there is nothing superfluous in it is an advantage, since it does not allow using several approaches in one project.

But if you are not confused by the shortcomings described in the article (quite possibly, these are purely my troubles), or there is no time to write your own routing, then you can safely use react-router.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *