Remix folder structure and ReScript modules

I’m creating an app with Remix

A typical folder structure looks like this:

├── routes/
│   ├── blog/
│   │   ├── $postId.tsx
│   │   ├── categories.tsx
│   │   ├── index.tsx
│   └── about.tsx
│   └── index.tsx
└── root.tsx

There are a couple of things that make writing these in ReScript tricky.

First is the $postId.tsx module. ReScript doesn’t accept this as a module name, even though the docs say

It is possible to use non-conventional characters in your filenames (which is sometimes needed for specific JS frameworks). Here are some examples:

  • src/Button.ios.res
  • pages/[id].res

Please note that modules with an exotic filename will not be accessible from other ReScript modules.

I hope that extra exotic patterns can be accepted in the future, so have raised a GitHub issue for this one

This can be worked around by creating a module at app/routes/blog/$postId/index.res which ReScript compiles. However this isn’t practical because you can have only one index.res module in the entire project.

Is there any way to solve this problem while still using file system based routing? Or do I need to fallback to using Remix config?

1 Like

I guess you found an edge-case with the $ character… the examples described in the docs should work.

Just like in NextJS, even if we’d support this kind of filename pattern, it would still be highly impractical because in non-trivial cases you will almost always have multiple $someId.res files in different subfolders, which is not possible due the unique-filename constraint.

Unfortunately the only way that worked for me is to keep the pages files in .tsx format and re-export ReScript generated code instead.


This is a shame, I’d love for ReScript to do better in these scenarios. Could this be solved by marking some modules as un-importable, maybe with a different file extension or prefix? This would solve all of the issues with exotic module names, including duplicate modules.

For example if there was a special file prefix ~ for marking a module as unimportable, the following structure could be possible.

├── routes/
│   ├── blog/
│   │   ├── ~$postId.res
│   │   ├── categories.res
│   │   ├── ~index.res
│   └── about.res
│   └── ~index.res
└── root.res

Modules prefixed with ~ would compile to JS files without the ~. eg. ~index.resindex.js. In other words, the prefix offers a generalised way to bypass the module name constraints completely.


@tom-sherman did you ever find a fix for this?

My current folder structure looks like this

├── res-routes/
│   ├── auth/
│   │   ├── Auth_Discord.res
│   │   ├── Auth_Discord.callback.res
│   └── Root_Index.res
├── routes/
│   ├── auth/
│   │   ├── discord.js
│   │   ├── discord.callback.js
│   └── index.js
└── root.res

Attempting to copy the import pattern in @ryyppy Next-Js-Template over to remix

Here are the contents of routes/index.js

import IndexRes from '~/res-routes/Root_Index.js'

export default props => {
  return <IndexRes {...props} />

I also removed the registerRoutes function in remix.config.js

 * @type {import('@remix-run/dev/config').AppConfig}
module.exports = {
  serverBuildTarget: 'vercel',
  server: process.env.NODE_ENV === 'development' ? undefined : './server.js',
  appDirectory: 'app',
  assetsBuildDirectory: 'public/build',
  publicPath: '/build/',
  serverBuildDirectory: 'build',
  devServerPort: 8002,
  ignoredRouteFiles: ['.*', '*.res'],
  transpileModules: ['rescript', 'rescript-webapi'],
  serverDependenciesToBundle: ['@rainbow-me/rainbowkit'],
  // routes(defineRoutes) {
  //   return defineRoutes(route => {
  //     registerRoutes(route)
  //   })
  // },

Edit it seems adding this function back fixed it. Not sure why its needed.

I’m getting this errors about initializing route modules. There was no error previously when using res-routes so there is no server code in my components.

Error: Cannot initialize 'routeModules'. This normally occurs when you have server code in your client modules.
Check this link for more details:
    at invariant2 (http://localhost:3000/build/_shared/chunk-5AT6NF26.js:1082:11)
    at RemixRoute (http://localhost:3000/build/_shared/chunk-5AT6NF26.js:2558:3)
    at renderWithHooks (http://localhost:3000/build/_shared/chunk-2QW45WNO.js:11065:26)
    at mountIndeterminateComponent (http://localhost:3000/build/_shared/chunk-2QW45WNO.js:13185:21)
    at beginWork (http://localhost:3000/build/_shared/chunk-2QW45WNO.js:13972:22)
    at HTMLUnknownElement.callCallback2 (http://localhost:3000/build/_shared/chunk-2QW45WNO.js:3675:22)
    at Object.invokeGuardedCallbackDev (http://localhost:3000/build/_shared/chunk-2QW45WNO.js:3700:24)
    at invokeGuardedCallback (http://localhost:3000/build/_shared/chunk-2QW45WNO.js:3734:39)
    at beginWork$1 (http://localhost:3000/build/_shared/chunk-2QW45WNO.js:17081:15)
    at performUnitOfWork (http://localhost:3000/build/_shared/chunk-2QW45WNO.js:16312:20)