How Remix's Flat File-Based Routing Works
Learn how Remix's flat file-based routing generates routes from file names.
Introduction
So you've tried adopting the Remix web framework before. But you gave up the idea because you felt the Remix default routing convention is a mess.
I feel you. That's how I almost gave it up too. The default routing in Remix isn't intuitive. Devs familiar with it will likely agree that it took some time and real effort to grasp.
In this article, I will be demystifying Remix's default routing convention. When I'm done, you will have a good understanding of how it works. Let's roll.
Prerequisite
This article assumes you're familiar with HTML, CSS, and JavaScript. If not, I recommend familiarizing yourself with these technologies before proceeding.
What is routing, route, and router in web development?
I can only imagine so much about you; after some point, all I can do is guess
. So, before talking about the fall of Rome, let's talk about Rome itself. That is, before we dive deep into how routing works in Remix, let's discuss what routing is.Routing is the process that determines how a website responds to different URLs. For example, how
example.com
responds toexample.com/abc
,example.com/abc/xyz
, etc.A route is a path on a website that points to a specific content or page. For example,
/abc
might be a route onexample.com
that points to the pageexample.com/abc
.A router is a tool that manages routing, determining which content or page to display based on the URL.
Web apps can be Multipage Applications (MPAs) or Single-Page Applications (SPAs). The terms routing, route, and router have different meanings based on the type of web app.
Aspect | Multipage Applications (MPAs) | Single Page Applications (SPAs) |
---|---|---|
Routing | The browser sends a request to the server for each new page, causing a full page reload. | Handled by JavaScript in the browser, updating content without a full reload. |
Route | Each route corresponds to a specific file or resource on the server (e.g., /about loads about.html ). | Routes trigger specific content to be rendered without reloading the entire page. |
Router | Server-side router matches the URL and serves the appropriate content. | Client-side router listens to URL changes and renders content dynamically. |
Browser History | Managed by the browser; each page reload adds a new entry. | Managed by the client-side router using history . |
"So, where does Remix stand?" you ask. Remix can build both! You can build your app with server-side rendering (SSR) for fast loads, SEO, and real-time data fetching, while still enjoying client-side routing like an SPA. Or, you can skip the server and use Remix in SPA mode.
What is the default convention for creating routes in Remix?
The heading of this section begs the question: Is there more than one way to create a route in Remix? The answer is YES. Remix, unlike many other frameworks, recognizes that routing isn't one-size-fits-all. So, it offers you the flexibility to choose what works best for your project or team. How thoughtful π₯Ή.
But, in the dev world: where there is no default, developers overthink. To prevent this, Remix provides a default convention. With this convention, you can start developing without worrying about routing decisions. The default is: create a JavaScript or TypeScript file inside the app/routes
folder, and you've got a route.
This approach is referred to as "flat-file based". This is because all routes are JavaScript (.js
/.jsx
) or TypeScript (.ts
/.tsx
) files, not folders. Even for nested routes where the intuitive thing would be to nest files in folders (as in Next.js), you still use files.
How does the flat file-based routing work in Remix?
Root route
The file app/root.tsx
is the root route. It serves as the root layout of the entire app. It's the first component in the UI that renders. The root route is the perfect place where you write styles, states, etc., you want to share across the entire app.
In Remix, an app can only have one root route (i.e. one root layout) shared across the entire app. This means, that unlike in Next.js where multiple root layouts can be created, this isn't possible in Remix. If you're developing an app that has sections with different layouts, there's a way of doing that in Remix. You'll find out soon.
With an app
directory like this:
app
βββ root.tsx
When you navigate to
/
, the UI renders in the following component hierarchy:
Basic routes
Creating a JavaScript (.js
/jsx
) or TypeScript (.ts
/.tsx
) file in the app/routes
folder generates a route in your app. The route's URL corresponds to the filename without the extension.
File | Matched route (URL) |
---|---|
app/routes/about.tsx | /about |
The exceptions to this are index routes:
File | Matched route (URL) |
---|---|
app/routes/_index.tsx | / |
app/routes/concerts._index.tsx | /concerts |
With an app
directory like this:
app
βββ root.tsx
βββ routes
βββ _index.tsx
βββ about.tsx
When you navigate to
/
, the UI renders in the following component hierarchy:When you navigate to
/about
, the UI renders in the following component hierarchy:
Dynamic routes
How do you create a route when you don't know the exact path ahead of time? Consider an app where users have unique URLs for accessing their dashboard. For example, Ayo Tunde accesses his dashboard at example.com/ayotunde
, Ada Mba at example.com/adamba
, etc.
These kinds of routes are generated from dynamic data that isn't known in advance. Hence, they're called dynamic routes. You create them by using the $
prefix.
File | Matched route (URL) |
---|---|
app/routes/$user.tsx | /ayotunde |
app/routes/$user.tsx | /adamba |
app/routes/$user.tsx | /8rv71k0hvg8 |
Note that you cannot have more than one dynamic route at the same level. For instance, having both app/routes/$user.tsx
and app/routes/$id.tsx
would cause a conflict. Unfortunately, Remix wonβt throw an error when you do.
With an app
directory like this:
app
βββ root.tsx
βββ routes
βββ $user.tsx
When you navigate to
/ayotunde
,/chiomamba
,/8rv71k0hvg8
, etc, the UI renders in the following component hierarchy:
Nested routes
How do you create routes with paths such as concerts/trending
, concerts/lagos
etc,.? These kind of routes are created with dot delimiters and are called nested routes.
By dot delimiters, I mean .
are used to separate the different parts of a route as it creates a /
in the URL. So, app/routes/concerts.trending.tsx
matches the route concerts/trending
.
But what does the .
separates the route into exactly? It separates them into the parent route and the child route. The part before the .
is the parent route, and matches a route with that filename. So, the route app/routes/concerts.trending.tsx
will have app/routes/concerts.tsx
as its parent route. The part after the .
is the nested child route, sharing the parent route as its layout.
When you create a nested route, ensure that there's a nested index route for the parent route. Otherwise, users will get a 404
error when they navigate to the parent URL.
With an app
directory like this:
app
βββ root.tsx
βββ routes
βββ concerts.$city.tsx
βββ concerts._index.tsx
βββ concerts.trending.tsx
βββ concerts.tsx
When you navigate to
/concerts
, the UI renders in the following component hierarchy:When you navigate to
/concerts/trending
, the UI renders in the following component hierarchy:When you navigate to
/concerts/lagos
,/concerts/benin
,/concerts/abuja
, etc, the UI renders in the following component hierarchy:
Nested routes without layout nesting
As previously discussed, nested routes automatically get their parent routes as their layout. So, what do you do in a case where you want a nested URL but don't want the child route to use the parent route as a nested layout? You opt out of the parent route with a trailing underscore (_
).
For example, the file app/routes/concerts_.mine.tsx
will create a route /concerts/mine
. But, instead of the UI hierarchy rendering as:
It will render as:
Note that, the trailing underscore (_
) only moves the child route out from the immediate parent route. So, for a route like app/routes/a.b_.c.tsx
, the UI hierarchy will render as:
To get the route a/b/c
to use the root route as the only layout, you will have to use _
twice: app/routes/a_.b_.c.tsx
. The UI hierarchy then renders it as:
Nested layouts without nested URLs
Consider an app that has a header and footer at the top and bottom of every route respectively. But, you want some routes to not have those headers and footers or have a completely different one. A common example of this is authentication pages. So, how do you create such routes?
You use pathless routes. These are routes that can share a layout with a group of routes without adding any path segments to the URL. They are created with leading underscore (_
).
For example, the route app/routes/_auth.tsx
is a pathless route. It can be used as a shared layout between the routes app/routes/_auth.signup.tsx
, app/routes/_auth.signin.tsx
, etc. You can visit those routes at /signup
and /signin
. The segment auth
isn't included in the URL. And, if you navigate to /auth
you'll get a 404
error because it's a pathless route.
Now, here's a very common mistake newbies to Remix make. They think the routes app/routes/_auth.signup.tsx
and app/routes/_auth.signin.tsx
will render as:
The correct component hierarchy that will be rendered in the UI is:
Why is this so? This is because the root route is the root layout of the entire app! It renders in the component hierarchy of every single route in your app. Hence, styles, states, and components you don't want every single route in your app to have shouldn't be included in the root route.
Optional Segments
Sometimes some path segments in your URL can be optional. A common example is in apps that can be viewed in different languages. In such an app, a user might navigate to say example.com
, and view the content in their default browser language. But, the user can also navigate to example.com/en
to also view the language in English.
You can create these optional segments by wrapping the route segment in parentheses (()
).
File | Matched route (URL) |
---|---|
app/routes/($lang)._index.tsx | / |
app/routes/($lang).categories.tsx | /categories |
app/routes/($lang).categories.tsx | /en/categories |
app/routes/($lang).categories.tsx | /fr/categories |
app/routes/($lang)._index.tsx | /bags |
app/routes/($lang).$productId.tsx | /en/bags |
app/routes/($lang).$productId.tsx | /fr/bags |
When using optional segments there are some stuff to watch out for:
An optional segment index route will be used in place of the index route for the root route. Say you have both
app/routes/($lang)._index.tsx
andapp/routes/_index.tsx
. When you navigate to/
, the UI will render in the following component hierarchy:Optional segments match eagerly. Say you have both
app/routes/($lang)._index.tsx
andapp/routes/($lang).$productId.tsx
. When you navigate to/bags
, the UI will render in the following component hierarchy:
Splat routes
The default behavior of websites is to throw a 404
error when a user navigates to a path not existing. For a great UX, you might redirect users to a valid route when they navigate to a non-existing one. These are pretty much the obvious ways when it comes to handling non-existing paths in an app.
But, if there comes a time when you want all routes to be valid in your app, Remix has got your back. You can have routes that match all URLs for your app, including non-existing ones $
character.
File | Matched route (URL) |
---|---|
app/routes/$.tsx | Matches virtually any URL for your app. |
app/routes/files.$.tsx | /files |
app/routes/files.$.tsx | files/books |
app/routes/files.$.tsx | files/books/things-fall-apart |
app/routes/files.$.tsx | files/123/abc/xyz/iii |
Folders
I know I said earlier that in flat file-based routing, only files, and not folders, can create routes. Well, that isn't entirely true. Remix recognizes that most times you would want to organize your code closer to the routes that use them. For example, a /projects
route using a ProjectList.tsx
, ProjectItem.tsx
, and ProjectCard.tsx
component. With a file-only approach, we can't do any organization. Remix gives us folders as routes for this.
A route can also be a folder if it contains a route.tsx
file. The route.tsx
file inside the folder is the root module and the rest of the other files in the folder do not become routes. So, we can organize our /projects
route into a folder as this, making the components used by the route closer to it:
app
βββ root.tsx
βββ routes
βββ projects
βββ ProjectCard.tsx
βββ ProjectItem.tsx
βββ ProjectList.tsx
βββ route.tsx
There are few things to note when using a folder as a route:
The route path is completely defined by the folder name. You use the same naming convention as you would have done if it was a file, omitting the extension. For example,
app/routes/projects._index
for the route/projects
,app/routes/projects.$projectId
for the route/app/projects/om4pt99jp7o
, etc.Nesting a folder inside another folder does not create a route. So the file
app/routes/projects/$projectId/route.tsx
does not create the route/projects/skgpu5j4h9o
. To do so, use the.
delimiter:app/routes/projects.$projectId/route.tsx
. In short, only top-level folders insideapp/routes
that contain aroute.tsx
file will create routes.
Escaping special characters
How do you create a route that has one of the special characters Remix uses for route conventions? You escape it with square brackets ([]
).
File | Matched route (URL) |
---|---|
app/routes/sitemap[.]xml.tsx | /sitemap.xml |
app/routes/[sitemap.xml].tsx | /sitemap.xml |
app/routes/weird-url.[_index].tsx | /weird-url/_index |
app/routes/dolla-bills-[$].tsx | app/routes/dolla-bills-[$].tsx |
app/routes/[[so-weird]].tsx | /[so-weird] |
Conclusion
Developers hate opinions but love defaults. Opinions handicap, while defaults empower. Remix knows this so well that it provides a default convention for routing. With this, you can spin up a Remix app and start developing it in no time. Together, we've gone through this default convention to make sense of it. And, I know you understand how it works better now.
This brings us to the conclusion of this great ride. Thanks for your time with me on the journey so far π«‘. I know it was a wild one, but I guess we both enjoyed it.
Remember, "hackers hack, crackers crack, and whiners whine. Be a hacker." Take care.