Simplifying Routing in React with Vite and File-based Routing
Francisco Mendes

Francisco Mendes @franciscomendes10866

About: I'm always brimming with ideas and love a good challenge.

Location:
Portugal
Joined:
Feb 12, 2020

Simplifying Routing in React with Vite and File-based Routing

Publish Date: Jan 22 '23
54 9

Introduction

One of the things that developers like most is to improve the flow of something that is repetitive, especially when we are talking about something with relatively simple needs, like the definition of a route in the application.

And in today's article we are going to create a structure where it will be possible to define our app's routes dynamically, taking into account the files that we have inside a specific folder.

Assumed knowledge

The following would be helpful to have:

  • Basic knowledge of React
  • Basic knowledge of React Router
  • Basic knowledge of Vite

Getting Started

The first step will be to start the application bootstrap.

Project Setup

Run the following command in a terminal:

yarn create vite app-router --template react
cd app-router
Enter fullscreen mode Exit fullscreen mode

Now we can install the necessary dependencies:

yarn add react-router-dom
Enter fullscreen mode Exit fullscreen mode

That's all we need in today's example, now we need to move on to the next step.

Bootstrap the Folder Structure

Let's pretend that the structure of our pages/ folder is as follows:

|-- pages/
   |-- dashboard/
      |--$id.jsx
      |-- analytics.jsx
      |-- index.jsx
   |-- about.jsx
   |-- index.jsx  
Enter fullscreen mode Exit fullscreen mode

From the snippet above we can reach the following conclusions:

  • The index namespace corresponds to the root of a route/sub route;
  • dashboard route has multiple sub routes;
  • The $ symbol means that a parameter is expected on that route.

With this in mind we can start talking about an amazing feature of Vite called Glob Import which serves to import several modules taking into account the file system.

Setup Router Abstraction

Before we start making code changes, I recommend creating a pattern, you can take into account known projects approaches and/or frameworks.

This is my recommendation to be easier to define the structure of the router, like for example which component should be assigned to the route? Is it expected to be possible to add an error boundary? Questions like this are important.

To show how it works, let's edit App.jsx, starting as follows:

// @/src/App.jsx
import { createBrowserRouter, RouterProvider } from "react-router-dom";

const pages = import.meta.glob("./pages/**/*.jsx", { eager: true });

// ...
Enter fullscreen mode Exit fullscreen mode

In the above code snippet we want to load all the modules that are present in the pages/ folder. What the glob() function will return is an object, whose keys correspond to the path of each module, and the value has properties with what is exported inside it.

// @/src/App.jsx
import { createBrowserRouter, RouterProvider } from "react-router-dom";

const pages = import.meta.glob("./pages/**/*.jsx", { eager: true });

const routes = [];
for (const path of Object.keys(pages)) {
  const fileName = path.match(/\.\/pages\/(.*)\.jsx$/)?.[1];
  if (!fileName) {
    continue;
  }

  // ...
}

// ...
Enter fullscreen mode Exit fullscreen mode

After having loaded all the modules present in the folder, we create an array called routes which will contain a list of objects with properties such as:

  • path - path that we want to register;
  • Element - React component we want to assign to the path;
  • loader - function responsible for fetching the data (optional);
  • action - function responsible for submit the form data (optional);
  • ErrorBoundary - React component responsible for catching JavaScript errors at the route level (optional).

And we need to get the name of the file, if this is not defined, we simply ignore it and move on to the next one. However, if we have a file name, let's normalize it so that we can register the routes.

// @/src/App.jsx
import { createBrowserRouter, RouterProvider } from "react-router-dom";

const pages = import.meta.glob("./pages/**/*.jsx", { eager: true });

const routes = [];
for (const path of Object.keys(pages)) {
  const fileName = path.match(/\.\/pages\/(.*)\.jsx$/)?.[1];
  if (!fileName) {
    continue;
  }

  const normalizedPathName = fileName.includes("$")
    ? fileName.replace("$", ":")
    : fileName.replace(/\/index/, "");

  // ...
}

// ...
Enter fullscreen mode Exit fullscreen mode

With the path now normalized we can append the data we have so far in the routes array, remembering that the component that will be assigned to the route has to be export default while all other functions (including the error boundary) have to be export. Like this:

// @/src/App.jsx
import { createBrowserRouter, RouterProvider } from "react-router-dom";

const pages = import.meta.glob("./pages/**/*.jsx", { eager: true });

const routes = [];
for (const path of Object.keys(pages)) {
  const fileName = path.match(/\.\/pages\/(.*)\.jsx$/)?.[1];
  if (!fileName) {
    continue;
  }

  const normalizedPathName = fileName.includes("$")
    ? fileName.replace("$", ":")
    : fileName.replace(/\/index/, "");

  routes.push({
    path: fileName === "index" ? "/" : `/${normalizedPathName.toLowerCase()}`,
    Element: pages[path].default,
    loader: pages[path]?.loader,
    action: pages[path]?.action,
    ErrorBoundary: pages[path]?.ErrorBoundary,
  });
}

// ...
Enter fullscreen mode Exit fullscreen mode

With the necessary data, we can now define each of the application's routes in the React Router by iterating over the routes array and assign the router to the Router Provider. Like this:

// @/src/App.jsx
import { createBrowserRouter, RouterProvider } from "react-router-dom";

// ...

const router = createBrowserRouter(
  routes.map(({ Element, ErrorBoundary, ...rest }) => ({
    ...rest,
    element: <Element />,
    ...(ErrorBoundary && { errorElement: <ErrorBoundary /> }),
  }))
);

const App = () => {
  return <RouterProvider router={router} />;
};

export default App;
Enter fullscreen mode Exit fullscreen mode

Conclusion

I hope you found this article helpful, whether you're using the information in an existing project or just giving it a try for fun.

Please let me know if you notice any mistakes in the article by leaving a comment. And, if you'd like to see the source code for this article, you can find it on the github repository linked below.

Github Repo

Comments 9 total

  • Andrej Gajdos
    Andrej GajdosMay 19, 2023

    How would you implement authenticated routes in this structure?

    • Francisco Mendes
      Francisco MendesMay 22, 2023

      Good question, or I would create an abstraction to create a parent route with nested routes, which would have contexts and everything else (CSR). Or I would go the simpler route which is to take advantage of loaders and actions to handle sessions, for that I would have to create an interface that handles that (similar to remix-auth).

  • Marco
    MarcoAug 25, 2023

    One way to integrate secure routes could be to define an array of routes and create a component () that handles the auth level

    const restricted: string[] = [
    "/dashboard",
    "/dashboard/analytics",
    "/dashboard/:id",
    ];

    const router = createBrowserRouter(
    routes.map(({ Element, ErrorBoundary, ...rest }) => ({
    ...rest,
    element: restricted.includes(rest.path) ? (
    <ProtectedRoot>
    <Element />
    </ProtectedRoot>
    ) : (
    <Element />
    ),
    errorElement: <ErrorHandler />,
    }))
    );

    • Timonwa Akintokun
      Timonwa AkintokunSep 1, 2023

      Great approach to handling secure routes! 👍 This makes routing in React with Vite much more straightforward. Thanks for sharing, Marco!

  • Lê Văn Thời
    Lê Văn ThờiSep 22, 2023

    Can you recreate the repo with typescript v5. Because, I created the project with vite and ts v5, when I deployed it it didn't work

  • Stephan D.D.
    Stephan D.D.Oct 16, 2023

    I love this - file based routing is so intuitive 😃 But... when I run the code everything works but after I build it my routes doesn't exists 🤔 Maybe its because the files are bundled into js chunks and therefore not existing when const pages: Pages = import.meta.glob('./pages/**/*.tsx', { eager: true }); looks for them - if this can be solve xmas will be nothing compared to this gift😃 - nice work

    • Stephan D.D.
      Stephan D.D.Oct 18, 2023

      I found the issue. I used serve to host my build but since it only has a index.html routes like /foo didn't exists. I ended up using af simple express server where every route * responded with the index.html - and then it worked 🕺

  • Charles Robertson
    Charles RobertsonMar 14, 2024

    I am using Remix, and it does something similar, but without the pages folder.

    In the:

    app/routes
    
    Enter fullscreen mode Exit fullscreen mode

    Folder, the files are named with dot annotation, like:

    app.uploadform1.$id.jsx
    
    Enter fullscreen mode Exit fullscreen mode

    This converts to:

    `/app/uploadform1/${uploadForm.id}`
    
    Enter fullscreen mode Exit fullscreen mode

    I was hoping that VSCode might have a plugin that updates the relevant routes, in the code, when someone changes the file name?

  • Phan Tấn Thắng
    Phan Tấn ThắngApr 18, 2024

    Thank you so much :)

Add comment