Customize Remix route convention

simonboisset

Simon Boisset

Posted on September 6, 2022

Customize Remix route convention

I love Remix for so many reasons. Whether its way of managing data fetching, its forms closer to pure html, or its nested routes.
One thing I particularly like is its customizable file system routing convention.
As you will see in this post, it is possible to define your own routing convention.

Remix flat routes

I discovered this possibility through the publication of an npm package remix-flat-routes
To be short it is enough to modify the configuration file remix.config.js

Remix feature routes

Personally I like to organize my files by feature. The problem with the previous conventions is that all the files present in the routes folder are effective routes. That's not what I want.
I want to be able to add method files for example in a feature but not considering this file as a route.
I wrote my own package for this convention: @remix-routes/feature
The routes will only be files in a folder or nested folder named routes if there is a folder in a routes folder then its page will be its index.tsx and its nested pages will be in a subfolder routes .
For more details, see the documentation.
To use it we define this convention in remix.config.js

const { defineFeatureRoutes } = require('@remix-routes/feature');
module. exports = {
  ignoredRouteFiles: ['**/*'],
  routes: async(defineRoutes) => {
    return defineFeatureRoutes('app', 'routes', 'routes', defineRoutes);
  },
};
Enter fullscreen mode Exit fullscreen mode

Customize your own convention

If you want to write your own convention I will explain you how to do it now.

Remix routes

To simplify remix routes convention writing I published a package @remix-routes/core

I will teach you how to use it then I will explain how it works.

Setup

First install this package

yarn add -D @remix-routes/core
Enter fullscreen mode Exit fullscreen mode

Then customize your remix.config.js

const { defineRemixRoutes } = require('@remix-routes/core');
module. exports = {
  ignoredRouteFiles: ['**/*'],
  routes: async(defineRoutes) => {
    return defineRemixRoutes(appDir, routesDir, defineRoutes, myConvention);
  },
};
Enter fullscreen mode Exit fullscreen mode

appDir is the name of remix app folder. Default is 'app'.

routesDir is the name of the routes folder. Default is 'routes'.

myConvention will be your custom function that take a list of file's names in the routes folder and return an object with route's ids as keys and files used for these routes as values.

Define your convention

Create your convention function which will create an Object files with route ids keys and file names values.

const myConvention = (routesDir: string, outletDir: string) => (filesList: string[]) => {
  let files: Record<string, string> = {};

  for (const file of filesList) {
    if (isRouteModuleFile(file) && isRouteFile(file, outletDir)) {
      let routeId = createRouteId(path.join(routesDir, file), outletDir);
      if (!files[routeId]) {
        files[routeId] = path.join(routesDir, file);
      } else {
        console.error('[Define routes] routeId is already defined :', routeId);
      }
    }
  }

  return files;
};
Enter fullscreen mode Exit fullscreen mode

Define which file is a route

import { stripFileExtension } from '@remix-routes/core';

function isRouteFile(filename: string, outletDir: string): boolean {
  const path = filename.split('/');
  if (path.length === 1) {
    return true;
  }
  if (path.length === 2 && stripFileExtension(path[1]) === 'index') {
    return true;
  }
  if (path[1] === outletDir && path[0] !== 'index') {
    return isRouteFile(path.slice(2).join('/'), outletDir);
  }

  return false;
}
Enter fullscreen mode Exit fullscreen mode

stripFileExtension return the file name without extension.

Create route id with file name

import { normalizeSlashes, stripFileExtension } from '@remix-routes/core';

function createRouteId(file: string, outletDir: string) {
  let path = normalizeSlashes(stripFileExtension(file));

  if (path.length > 2 && path[path.length - 1] === 'index') {
    if (path[path.length - 2] === outletDir) {
      path.splice(-2, 1);
    } else {
      path.splice(-1, 1);
    }
  }
  path = path.filter((name, i) => i === 0 || name !== outletDir);

  return path.join('/');
}

Enter fullscreen mode Exit fullscreen mode

normalizeSlashes return an array of his router path.

Example :
'routes/auth/login' => ['routes', 'auth', 'login']
'routes/auth.logout' => ['routes', 'auth', 'logout']

Explanations

Finally we will see how it happens in detail.
The routes property of the remix configuration expects a method that takes in parameters a method that allows to define its routes.
We will have to call this method each time we want to define a route for a file.
The main idea is to browse the folders to list all the files to include with the url that will be assigned to them and then to call the defineRoutes method to declare it for the configuration.

@remix-routes/core will visites all filles in the routes folder and list them for your convention callback. Then it will get your convention result object to define remix routes and nested routes based on your route ids.

Conclusion

I hope you like this article.
Now that you know how to define your own convention, it's up to you to create your own.

💖 💪 🙅 🚩
simonboisset
Simon Boisset

Posted on September 6, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related