Gotchas

As we’ve built Remix, we’ve been laser focused on production results and scalability for your users and team working in it. Because of this, some developer-experience and ecosystem-compatibility issues exist that we haven’t smoothed over yet.

This document should help you get over these bumps.

Server Code in Client Bundles

You may run into this strange error in the browser. It almost always means that server code made it into browser bundles.

  1. TypeError: Cannot read properties of undefined (reading 'root')

For example, you can’t import “fs-extra” directly into a route module:

  1. import { json } from "@remix-run/node"; // or cloudflare/deno
  2. import fs from "fs-extra";
  3. export async function loader() {
  4. return json(await fs.pathExists("../some/path"));
  5. }
  6. export default function SomeRoute() {
  7. // ...
  8. }

To fix it, move the import into a different module named *.server.js or *.server.ts and import from there. In our example here, we create a new file at utils/fs-extra.server.js:

  1. export { default } from "fs-extra";

And then change our import in the route to the new “wrapper” module:

  1. import { json } from "@remix-run/node"; // or cloudflare/deno
  2. import fs from "~/utils/fs-extra.server";
  3. export async function loader() {
  4. return json(await fs.pathExists("../some/path"));
  5. }
  6. export default function SomeRoute() {
  7. // ...
  8. }

Even better, send a PR to the project to add "sideEffects": false to their package.json so that bundlers that tree shake know they can safely remove the code from browser bundles.

Similarly, you may run into the same error if you call a function at the top-level scope of your route module that depends on server-only code.

For example, Remix upload handlers like unstable_createFileUploadHandler and unstable_createMemoryUploadHandler use Node globals under the hood and should only be called on the server. You can call either of these functions in a *.server.js or *.server.ts file, or you can move them into your route’s action or loader function.

So instead of doing:

  1. import { unstable_createFileUploadHandler } from "@remix-run/node"; // or cloudflare/deno
  2. const uploadHandler = unstable_createFileUploadHandler({
  3. maxPartSize: 5_000_000,
  4. file: ({ filename }) => filename,
  5. });
  6. export async function action() {
  7. // use `uploadHandler` here ...
  8. }

You should be doing:

  1. import { unstable_createFileUploadHandler } from "@remix-run/node"; // or cloudflare/deno
  2. export async function action() {
  3. const uploadHandler = unstable_createFileUploadHandler({
  4. maxPartSize: 5_000_000,
  5. file: ({ filename }) => filename,
  6. });
  7. // use `uploadHandler` here ...
  8. }

Why does this happen?

Remix uses “tree shaking” to remove server code from browser bundles. Anything inside of Route module loader, action, and headers exports will be removed. It’s a great approach but suffers from ecosystem compatibility.

When you import a third-party module, Remix checks the package.json of that package for "sideEffects": false. If that is configured, Remix knows it can safely remove the code from the client bundles. Without it, the imports remain because code may depend on the module’s side effects (like setting global polyfills, etc.).

Importing ESM Packages

You may try importing an ESM-only package into your app and see an error like this when server rendering:

  1. Error [ERR_REQUIRE_ESM]: require() of ES Module /app/node_modules/dot-prop/index.js from /app/project/build/index.js not supported.
  2. Instead change the require of /app/project/node_modules/dot-prop/index.js in /app/project/build/index.js to a dynamic import() which is available in all CommonJS modules.

To fix it, add the ESM package to the serverDependenciesToBundle option in your remix.config.js file.

In our case here, we’re using the dot-prop package, so we would do it like this:

  1. /** @type {import('@remix-run/dev').AppConfig} */
  2. module.exports = {
  3. serverDependenciesToBundle: ["dot-prop"],
  4. // ...
  5. };

Why does this happen?

Remix compiles your server build to CJS and doesn’t bundle your node modules. CJS modules can’t import ESM modules.

Adding packages to serverDependenciesToBundle tells Remix to bundle the ESM module directly into the server build instead of requiring it at runtime.

Isn’t ESM the future?

Yes! Our plan is to allow you to compile your app to ESM on the server. However, that will come with the reverse problem of not being able to import some CommonJS modules that are incompatible with being imported from ESM! So even when we get there, we may still need this configuration.

You may ask why we don’t just bundle everything for the server. We could, but that will slow down builds and make production stack traces all point to a single file for your entire app. We don’t want to do that. We know we can smooth this over eventually without making that tradeoff.

With major deployment platforms now supporting ESM server side, we’re confident the future is brighter than the past here. We’re still working on a solid dev experience for ESM server builds, our current approach relies on some things that you can’t do in ESM. We’ll get there.

typeof window checks

Because the same JavaScript code can run in the browser as well as the server, sometimes you need to have a part of your code that only runs in one context or the other:

  1. if (typeof window === "undefined") {
  2. // running in a server environment
  3. } else {
  4. // running in a browser environment
  5. }

This works fine in a Node.js environment, however, Deno actually supports window! So if you really want to check whether you’re running in the browser, it’s better to check for document instead:

  1. if (typeof document === "undefined") {
  2. // running in a server environment
  3. } else {
  4. // running in a browser environment
  5. }

This will work for all JS environments (Node.js, Deno, Workers, etc.).

Browser extensions injecting code

You may run into this warning in the browser:

  1. Warning: Did not expect server HTML to contain a <script> in <html>.

This is a hydration warning from React, and is most likely due to one of your browser extensions injecting scripts into the server-rendered HTML, creating a difference with the resulting HTML.

Check out the page in incognito mode, the warning should disappear.

CSS bundle being incorrectly tree-shaken

When using CSS bundling features in combination with export * (e.g. when using an index file like components/index.ts that re-exports from all sub-directories) you may find that styles from the re-exported modules are missing from the build output.

This is due to an issue with esbuild’s CSS tree shaking. As a workaround, you should use named re-exports instead.

  1. -export * from "./Button";
  2. +export { Button } from "./Button";

Note that, even if this issue didn’t exist, we’d still recommend using named re-exports! While it may introduce a bit more boilerplate, you get explicit control over the module’s public interface rather than inadvertently exposing everything.