Remix CLI (v2)

The Remix CLI comes from the @remix-run/dev package. It also includes the compiler. Make sure it is in your package.json devDependencies so it doesn’t get deployed to your server.

To get a full list of available commands and flags, run:

  1. npx @remix-run/dev -h

remix build

Builds your app for production. This command will set process.env.NODE_ENV to production and minify the output for deployment.

  1. remix build

Options

Optionflagconfigdefault
Generate sourcemaps for production build—sourcemapN/Afalse

remix dev

Runs the Remix compiler in watch mode and spins up your app server.

The Remix compiler will:

  1. Set NODE_ENV to development
  2. Watch your app code for changes and trigger rebuilds
  3. Restart your app server whenever rebuilds succeed
  4. Send code updates to the browser via Live Reload and HMR + Hot Data Revalidation

🎥 For an introduction and deep dive into HMR and HDR in Remix, check out our videos:

What is “Hot Data Revalidation”?

Like HMR, HDR is a way of hot updating your app without needing to refresh the page. That way you can keep your app state as your edits are applied in your app. HMR handles client-side code updates like when you change the components, markup, or styles in your app. Likewise, HDR handles server-side code updates.

That means any time your change a loader on your current page (or any code that your loader depends on), Remix will re-fetch data from your changed loader. That way your app is always up-to-date with the latest code changes, client-side or server-side.

To learn more about how HMR and HDR work together, check out Pedro’s talk at Remix Conf 2023.

With remix-serve

Enable the v2_dev:

  1. /** @type {import('@remix-run/dev').AppConfig} */
  2. module.exports = {
  3. future: {
  4. v2_dev: true,
  5. },
  6. };

That’s it!

With custom app server

If you used a template to get started, hopefully it’s integrated with v2_dev out-of-the-box. If not, you can follow these steps to integrate your project with v2_dev:

  1. Enable the v2_dev:
  1. /** @type {import('@remix-run/dev').AppConfig} */
  2. module.exports = {
  3. future: {
  4. v2_dev: true,
  5. },
  6. };
  1. Replace your dev scripts in package.json and use -c to specify your app server command:
  1. {
  2. "dev": "remix dev -c \"node ./server.js\""
  3. }
  1. Ensure broadcastDevReady is called when your app server is up and running:
  1. import path from "node:path";
  2. import { broadcastDevReady } from "@remix-run/node";
  3. import express from "express";
  4. const BUILD_DIR = path.resolve(__dirname, "build");
  5. const build = require(BUILD_DIR);
  6. const app = express();
  7. // ... code for setting up your express app goes here ...
  8. app.all(
  9. "*",
  10. createRequestHandler({
  11. build,
  12. mode: process.env.NODE_ENV,
  13. })
  14. );
  15. const port = 3000;
  16. app.listen(port, () => {
  17. console.log(`👉 http://localhost:${port}`);
  18. if (process.env.NODE_ENV === "development") {
  19. broadcastDevReady(build);
  20. }
  21. });

For CloudFlare, use logDevReady instead of broadcastDevReady.

Why? broadcastDevReady uses fetch to send a ready message to the Remix compiler, but CloudFlare does not support async I/O like fetch outside of request handling.

Options

Options priority order is: 1. flags, 2. config, 3. defaults.

Optionflagconfigdefaultdescription
Command-c / —commandcommandremix-serve <server build path>Command used to run your app server
Manual—manualmanualfalseSee guide for manual mode
Port—portportDynamically chosen open portInternal port used by the Remix compiler for hot updates
TLS key—tls-keytlsKeyN/ATLS key for configuring local HTTPS
TLS certificate—tls-certtlsCertN/ATLS certificate for configuring local HTTPS

To set options in your config, replace v2_dev: true with an object. For example:

  1. /** @type {import('@remix-run/dev').AppConfig} */
  2. module.exports = {
  3. future: {
  4. v2_dev: {
  5. // ...any other options you want to set go here...
  6. manual: true,
  7. tlsKey: "./key.pem",
  8. tlsCert: "./cert.pem",
  9. },
  10. },
  11. };

Setting a custom port

The remix dev --port option sets the internal port used for hot updates. It does not affect the port your app runs on.

To set your app server port, set it the way you normally would in production. For example, you may have it hardcoded in your server.js file.

If you are using remix-serve as your app server, you can use its --port flag to set the app server port:

  1. remix dev -c "remix-serve --port 8000 ./build"

In contrast, the remix dev --port option is an escape-hatch for users who need fine-grain control of network ports. Most users, should not need to use remix dev --port.

Manual mode

By default, remix dev will restart your app server whenever a rebuild occurs. If you’d like to keep your app server running without restarts across rebuilds, check out our guide for manual mode.

You can see if app server restarts are a bottleneck for your project by comparing the times reported by remix dev:

  • rebuilt (Xms) 👉 the Remix compiler took X milliseconds to rebuild your app
  • app server ready (Yms) 👉 Remix restarted your app server and it took Y milliseconds to start with the new code changes

Pick up changes from other packages

If you are using a monorepo, you might want Remix to perform hot updates not only when your app code changes, but whenever you change code in any of your apps dependencies.

For example, you could have a UI library package (packages/ui) that is used within your Remix app (packages/app). To pick up changes in packages/ui, you can configure watchPaths to include your packages.

How to set up MSW

To use Mock Service Worker in development, you’ll need to:

  1. Run MSW as part of your app server
  2. Configure MSW to not mock internal “dev ready” messages to the Remix compiler

Make sure that you are setting up your mocks for your app server within the -c flag so that the REMIX_DEV_HTTP_ORIGIN environment variable is available to your mocks. For example, you can use NODE_OPTIONS to set Node’s --require flag when running remix-serve:

  1. {
  2. "scripts": {
  3. "dev": "remix dev -c \"npm run dev:app\"",
  4. "dev:app": "cross-env NODE_OPTIONS=\"--require ./mocks\" remix-serve ./build"
  5. }
  6. }

Next, you can use REMIX_DEV_ORIGIN to let MSW forward internal “dev ready” messages on /ping:

  1. import { rest } from "msw";
  2. const REMIX_DEV_PING = new URL(
  3. process.env.REMIX_DEV_ORIGIN
  4. );
  5. REMIX_DEV_PING.pathname = "/ping";
  6. export const server = setupServer(
  7. rest.post(REMIX_DEV_PING.href, (req) => req.passthrough())
  8. // ... other request handlers go here ...
  9. );

How to set up local HTTPS

For this example, let’s use mkcert. After you have it installed, make sure to:

  • Create a local Certificate Authority if you haven’t already done so
  • Use NODE_EXTRA_CA_CERTS for Node compatibility
  1. mkcert -install # create a local CA
  2. export NODE_EXTRA_CA_CERTS="$(mkcert -CAROOT)/rootCA.pem" # tell Node to use our local CA

Now, create the TLS key and certificate:

  1. mkcert -key-file key.pem -cert-file cert.pem localhost

👆 You can change localhost to something else if you are using custom hostnames.

Next, use the key.pem and cert.pem to get HTTPS working locally with your app server. This depends on what you are using for your app server. For example, here’s how you could use HTTPS with an Express server:

  1. import fs from "node:fs";
  2. import https from "node:https";
  3. import path from "node:path";
  4. import express from "express";
  5. const BUILD_DIR = path.resolve(__dirname, "build");
  6. const build = require(BUILD_DIR);
  7. const app = express();
  8. // ... code setting up your express app goes here ...
  9. const server = https.createServer(
  10. {
  11. key: fs.readFileSync("path/to/key.pem"),
  12. cert: fs.readFileSync("path/to/cert.pem"),
  13. },
  14. app
  15. );
  16. const port = 3000;
  17. server.listen(port, () => {
  18. console.log(`👉 https://localhost:${port}`);
  19. if (process.env.NODE_ENV === "development") {
  20. broadcastDevReady(build);
  21. }
  22. });

Now that the app server is set up, you should be able to build and run your app in production mode with TLS. To get the Remix compiler to interop with TLS, you’ll need to specify the TLS cert and key you created:

  1. remix dev --tls-key=key.pem --tls-cert=cert.pem -c "node ./server.js"

Alternatively, you can specify the TLS key and cert via the v2_dev.tlsCert and v2_dev.tlsKey config options. Now your app server and Remix compiler are TLS ready!

How to integrate with a reverse proxy

Let’s say you have the app server and Remix compiler both running on the same machine:

  • App server 👉 http://localhost:1234
  • Remix compiler 👉 http://localhost:5678

Then, you setup a reverse proxy in front of the app server:

  • Reverse proxy 👉 https://myhost

But the internal HTTP and WebSocket connections to support hot updates will still try to reach the Remix compiler’s unproxied origin:

  • Hot updates 👉 http://localhost:5678 / ws://localhost:5678

To get the internal connections to point to the reverse proxy, you can use the REMIX_DEV_ORIGIN environment variable:

  1. REMIX_DEV_ORIGIN=https://myhost remix dev

Now, hot updates will be sent correctly to the proxy:

  • Hot updates 👉 https://myhost / wss://myhost

Performance tuning and debugging

Path imports

Currently, when Remix rebuilds your app, the compiler has to process your app code along with any of its dependencies. The compiler treeshakes unused code from app so that you don’t ship any unused code to browser and so that you keep your server as slim as possible. But the compiler still needs to crawl all the code to know what to keep and what to treeshake away.

In short, this means that the way you do imports and exports can have a big impact on how long it takes to rebuild your app. For example, if you are using a library like Material UI or AntD you can likely speed up your builds by using path imports:

  1. - import { Button, TextField } from '@mui/material';
  2. + import Button from '@mui/material/Button';
  3. + import TextField from '@mui/material/TextField';

In the future, Remix could pre-bundle dependencies in development to avoid this problem entirely. But today, you can help the compiler out by using path imports.

Debugging bundles

Dependending on your app and dependencies, you might be processing much more code than your app needs. Check out our bundle analysis guide for more details.

Troubleshooting

HMR: hot updates losing app state

Hot Module Replacement is supposed to keep your app’s state around between hot updates. But in some cases React cannot distinguish between existing components being changed and new components being added. React needs keys to disambiguate these cases and track changes when sibling elements are modified.

Additionally, when adding or removing hooks, React Refresh treats that as a brand-new component. So if you add useLoaderData to your component, you may lose state local to that component.

These are limitations of React and React Refresh, not Remix.

HDR: every code change triggers HDR

Hot Data Revalidation detects loader changes by trying to bundle each loader and then fingerprinting the content for each. It relies on tree shaking to determine whether your changes affect each loader or not.

To ensure that tree shaking can reliably detect changes to loaders, make sure you declare that your app’s package is side effect free:

  1. {
  2. "sideEffects": false
  3. }

HDR: harmless console errors when loader data is removed

When you delete a loader or remove some of the data being returned by that loader, your app should be hot updated correctly. But you may notice console errors logged in your browser.

React strict-mode and React Suspense can cause multiple renders when hot updates are applied. Most of these render correctly, including the final render that is visible to you. But intermediate renders can sometimes use new loader data with old React components, which is where those errors come from.

We are continuing to investigate the underlying race condition to see if we can smooth that over. In the meantime, if those console errors bother you, you can refresh the page whenever they occur.

HDR: performance

When the Remix compiler builds (and rebuilds) your app, you may notice a slight slowdown as the compiler needs to crawl the dependencies for each loader. That way Remix can detect loader changes on rebuilds.

While the initial build slowdown is inherently a cost for HDR, we plan to optimize rebuilds so that there is no perceivable slowdown for HDR rebuilds.