Migrating Remix to Vite

Fernando Abolafio
5 min readMar 26, 2024


Challenges, learnings, and scripts — still not done yet 😰

At Growblocks, we are still migrating our Remix application to Vite. It hasn’t been easy, but we are fighting tirelessly with it.

A bit of background

Our Remix application is about 2.5 years old, so it has experienced and survived much of the framework's massive (and positive) transformation over the past years.

We serve several customers with it and have multiple developers working on it. Overall, we are happy with our choice and are not looking back.

But one of the drawbacks of using a framework so new is going through some of the early big changes. That’s how we get into the Vite Migration.

Remix meets Vite

In October 2023, Remix officially announced they unstable support of Vite. You can read the full article in the link, but a summary of the why is:

Remix is migrating to Vite for faster development, a rich plugin ecosystem, and improved speed, with features like 10x faster HMR and 5x faster HDR. Vite's design allows Remix to transition to a Vite plugin, streamlining development processes. This strategic move enhances efficiency and flexibility for Remix users, marking a significant step forward in web development.

Let’s migrate then

Fast forward to February 2024, and Vite is now stable in the eyes of Remix 🎉.

At about the same time, one of the primary references of a good Remix repo in the community, the Epic Stack, migrated to Vite. And it seemed quite straightforward.

So we say, “Let’s give it a try”. It should work.

It has been about a month. Truthfully, we haven’t invested a lot of time in this migration, but it hasn’t been a smooth ride.

The issues we’ve encountered

The migration guide is straightforward so that I won’t cover the obvious issues but rather the hairy hidden ones that only a project that is somewhat of a legacy in such a new framework can have. Yep, it sounds weird.

The requested module ‘/app/root.tsx’ does not provide an export named ‘default’

This was by far the most hidden 💩 we had.

We were getting this strange error in the browser; every time the JS was loaded, it wouldn’t run with this message. No further context was given except it pointed to a random place in the root file. Not even to the export line.

We tried all the different approaches to debugging you can imagine. Our first suspect was some bad splitting of server and client code, which is a hard no-go from Vite.

Straight to the Eureka moment:

Thanks to the vite-plugin-inspect I observed it had step in the vite processing of the code that was done by remix-react-refresh-babel

So that was in the back of my head, and then I compared the exports coming out of the root file from the epic-stack browser code with ours.

Well, they were pretty different. So, something happened during this step.

From there, it was easy to notice our babel.config.json file, which was used by jest to process TypeScript.

Solution: delete the babel file. ✅ 😮‍💨

Splitting of server and client code

As expected, a lot of our code base didn’t conform to Vite’s expectations of splitting server and client-side code. There were two big issues that needed to be solved first:

Remove rmx-cli dependency

we were using the rmx-cli to generate a single remix file that would export all remix packages at once. From @remix-run/node , @remix-run/react , remix-typedjson . This decision was made a long time ago and back in the time it served it purpose well. Now, having everything imported from this file meant we were loading server code from @remix-run/node everywhere. So we had to revert it and import it directly from the original packages.

For that, I wrote a script, which can be found here.
It was able to accomplish the following:

Solution: write a script that reverts all imports and drops the rmx-cli dependency.

Import types everywhere

To prevent further imports of server code in the client, we added the linter rule of consistent-type-imports , which enforces that we prefix our module imports with type when that’s what it is. This allows bundlers, such as Vite, to drop any module during the three-shaking process that has only type been imported from it. There are also other benefits, you can read in here.

So, the solution was to add this linter rule and fix all issues across the codebase. ✅

What is next

We are not done yet; the HMR isn’t working properly, and we can see many JS code downloaded in the client. Now that the Babel config is gone, we still need to fix Jest. But all of these seem more achievable than the ones listed here. So, for now, the future looks bright.

Remix has helped us move fast, and it has a lot of credit with us on these early migrations of the framework, which frankly is for the best. The Remix team has done a great job, and we are thankful for it. At the end of the day, I’m sure we will be satisfied with the faster build times and testing runs once we have Vitest running.

Dealing with modern web stacks can be quite challenging, especially when it comes to talking bundlers. It's an unpredictable process that can be quite messy at times, but it's important to embrace the challenges and learn from them. While it can be a painful experience, it's necessary for the growth of the framework and the application.



Fernando Abolafio

Programmer — Modeling Revenue in code @GrowblocksHQ — Writing about Web Dev/React/Remix/NextJs — Software Architecture