I recently migrated a Next.js project to Remix in order to improve performance and maintainability.
These are 8 reasons why you should and should not use Remix.
The migrated Next.js app can be described as follows:
After the migration to Remix and using Cloudflare Workers as deployment target, following changed:
The migration itself was mostly straightforward with minor hiccups around SSR third-party React components.
Edit (3. November 2022): A lot has happened in the last 3 months and NextJS 13 has layouts with a similar API to Remix. And the whole thing builds faster too thanks to Turbopack.
Thanks Jannis Milz for pointing it out in the comments!
The Next.js app had a clean separation of pages and components. Each component contained its own style and logic.
Some of the logic was extracted using hooks for easier re-use across multiple components.
Both Next.js and Remix are React frameworks with decent support for Tailwind. After setting up the initial structure, migrating the
React components was as easy as going through all of their dependencies and replacing every package with a @next scope.
Next.js provides a solid Image component component that works out of the box. The component does quite a bit of heavy lifting and I only realized its scope when I had to find a replacement.
Remix does not come with a comparable solution. At the time of writing, there is remix-image.
<Image
src="https://asset.example.org/image.png"
responsive={[
{
size: { width: 100, height: 100 },
maxWidth: 500,
},
{
size: { width: 600, height: 600 },
},
]}
dprVariants={[1, 3]}
/>
I decided to use the asset service that was provided by the client to create image renditions (= different sizes of the same image).
Using the renditions and setting srcset manually, I ended up with a smaller, less powerful custom Image component.
Both Remix and Next.js use file-based routing. To create a path hierarchy you need to create files that export React components (and other things).
Both frameworks come with their Link components to abstract away client-side routing and data fetching. Replacing the Next.js version with the Remix version was trivial.
Remix brings nested routes to the table. Strictly speaking, the router of Remix can do everything that the router of Next.js can do.
It’s possible to keep the routes and the file structure of a Next.js app and just keep using them with Remix.
However, in order to take advantage of proper error propagation and better data reading for child pages, I switched to Remix’s Outlet for nested pages. Outlets are a way of telling components where to render their children.
The children pages bring their own loader and action functions. They know how to read their own data and they provide the logic to handle data writes such as form submissions.
This allowed me to replace custom client-server communication (lots of manual POST) with fetcher or even <Form>
.
Remix gives you “loading” or “pending” states for free. There is no need to keep track of the state anymore by doing something like this:
const [isLoading, setIsLoading] = useState<boolean>(false);
const [isSubmitted, setIsSubmitted] = useState<boolean>(false);
useEffect(() => {
if (isSubmitted) {
setIsLoading(true)
// submit here
.finally(() => setIsLoading(false));
setIsLoading(false);
}
}, [isSubmitted]);
The most painful part of the migration was server-side rendering (SSR) of third-party components (= React components from NPM). In the browser, both Next.js and Remix are just running React.
Consequentially, what works in the browser on Next.js works on Remix.
Rendering on the server, on the other hand, is painful. Many third party React components have a section about SSR.
Unfortunately, the instructions are valid for Next.js, sometimes for Gatsby. It seems that Remix is not yet a first-class React framework among React developers.
The component that I am still not able to render on the server is react-select because of its use of emotion.
A workaround using <ClientOnly />
from remix-utils fixed the issue.
import Select from "react-select";
import { ClientOnly } from "remix-utils";
const CustomDropdown = () => {
return (
<ClientOnly>
<Select />
</ClientOnly>
);
};
It is important to render placeholders or empty components to avoid layout shifts when the client-side rendering kicks in.
In this case, the width depends on the rendered data. Hard coding w-64 fixed the issue.
import Select from "react-select";
import { ClientOnly } from "remix-utils";
const CustomDropdown = () => {
return (
<div className="w-64">
<ClientOnly>
<Select />
</ClientOnly>
</div>
);
};
Both are great frameworks that make developing React apps a joy.
Remix was born out of the mature React Router library but is much younger and more cutting-edge than Next.js.
The documentation is good, but if things go wrong you better be ready to hop on Discord or read the source code. Reading the source code of the frameworks you use is a good idea anyway 😛.
Nested routes and a unified way to read and write data are big steps in the right direction.
The ecosystem is innovating top of the novel yet solid abstractions. Some third-party React components that work with Next.js are still painful to use with Remix on the server.
Erben Systems GmbH
Watterstrasse 81, c/o Sarbach Treuhand AG, 8105 Regensdorf, Switzerland
CHE-174.268.027 MwSt