Skip to content

Releases: remix-run/remix

v0.19.0-pre.1

07 Oct 22:50
Compare
Choose a tag to compare
v0.19.0-pre.1 Pre-release
Pre-release
Version 0.19.0-pre.1

v0.19.0-pre.0

07 Oct 22:02
Compare
Choose a tag to compare
v0.19.0-pre.0 Pre-release
Pre-release
Version 0.19.0-pre.0

v0.18.2

18 Sep 00:00
Compare
Choose a tag to compare

This release pins React Router to v6.0.0-beta.0 which fixes an issue we were having running Remix on the latest React Router beta release. We will update Remix to be able to run on the latest React Router beta releases next week.

v0.18.0

17 Sep 21:08
Compare
Choose a tag to compare

Well, summertime is winding up here in the Northern Hemisphere which means it's time for us to unleash everything we've been working on between family vacations and backwoods camping trips. And let me tell you: it's a LOT of stuff. Buckle your seat belts.

tl;dr Upgrade Guide

  • Upgrade the remix and all @remix-run/* packages to 0.18
  • Add remix setup to your package.json postinstall
  • Add { "paths": { "~/*": ["./app/*"] } } to your tsconfig's compilerOptions
  • If you're using TypeScript:
    • Sorry, you're gonna have to go read the part about our TypeScript improvements!
  • Enjoy the rest of your day

Where's the Transition Stuff?

First of all, I know that some of you have been following the work that Ryan released in v0.18.0-pre.0 and were expecting it to be released in v0.18, however that work is still being refined so we backed it out for this release. We expect it to be released in v0.19 very soon. We just didn't want it to block us releasing all the goodies we've had piling up while it settles.

Sourcemaps

The Remix compiler now outputs sourcemaps for your code on both the server and in the browser. Stack traces on the server will now give you a message in the console that links back to your original source code. This is especially handy when you're using a terminal emulator like iTerm 2 or VS Code that supports ctrl-click on file paths to open a file. Sourcemaps in the browser work as you'd expect.

Huge thank-you to kiliman who got the ball rolling on this one by patching the Remix source code!

Compiler Improvements

The Remix compiler now includes support for using ~ as an import alias for stuff in your app directory. We built this feature by piggy-backing on top of TypeScript's existing convention for path mapping, so the alias is actually defined in your app/tsconfig.json file (or app/jsconfig.json file if you're not using TypeScript).

This means that when app/routes/users/$id.tsx imports app/utils.ts, it can import utils from "~/utils" instead of import utils from "../../../utils".

Note: For now the only alias our compiler supports is ~. We may add support for more in the future.

To add support for this to an existing app, add the following to your tsconfig.json:

{
  "compilerOptions": {
    "paths": {
      "~/*": ["./app/*"]
    }
  }
}

Other things the compiler now supports include:

  • dynamic import()
  • importing .css files from packages in node_modules

We also fixed a bug where the compiler would crash when you had syntax errors and we now clean up all the files the compiler generates in dev mode when it shuts down.

TypeScript Improvements

A major goal for us is to have great support for TypeScript out of the box in Remix. This release further improves our TypeScript support by adding a remix.env.d.ts file to the project root. This will be present automatically in new projects created with npm init remix.

The remix.env.d.ts file references all the Remix types that you will need in your project. Previous to this release our types were made available to the TypeScript compiler whenever you imported something from remix. But if you didn't, you weren't able to use them.

If you're upgrading an existing project, add the following to your tsconfig.json:

{
  "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"]
}

And put the following in remix.env.d.ts in your root directory:

/// <reference types="@remix-run/dev" />
/// <reference types="@remix-run/node/globals" />

Please note that we switched from using the node-fetch types in your node projects to using the built-in DOM types for things like Headers, Request, Response, and fetch. We took this approach because our route modules run in both the browser and on the server, so you'll most likely need the DOM types anyway. And since we can't overwrite the DOM types for these globals, it's better to piggyback on them.

If you need to do node-specific stuff, you can always type cast to get the type you need. For example, if you need to stream the body of an incoming request for doing things like file uploads, you could do something like this to get the right types:

export function loader({ request }) {
  ((request.body as unknown) as NodeJS.ReadableStream).pipe(...);
}

Note: We plan on having much better support for file uploads in an upcoming release, but this should get you by for now.

MDX

Remix v0.18 includes built-in support for handling MDX files, both as route modules (in app/routes) and in import statements. This is great for simple use cases of MDX where you don't have a lot of files.

Read more about our MDX support in the docs.

Netlify Functions

The npm init remix command now supports Netlify Functions as a development and deploy target. As usual, just follow the instructions in the README of your new Remix app to get up and running on Netlify in just a few minutes.

New remix setup Command

The remix CLI has a new command called remix setup for setting up your node_modules/remix directory with all the right files you need in your app.

When we introduced the remix package in v0.17, we relied on postinstall scripts in our various packages to automatically setup the node_modules/remix package with everything it needs so you don't have to remember where to import stuff from. Instead, you can just import it all from remix. However, separate postinstall scripts in our own packages are not guaranteed to run after you have installed everything you need for your app, including other @remix-run/* packages.

The remix setup command is a better solution. Instead of relying on our own package postinstall scripts, remix setup should run in your app's postinstall hook. This means it runs after all of your app's dependencies are installed. It should also be resilient to the way different package managers (npm, yarn, pnpm) do things when installing and removing packages.

This command is already included in the Remix init templates. If you have an existing app that you're upgrading, just add the following to your package.json:

{
  "scripts": {
    "postinstall": "remix setup"
  }
}

Support for Multiple Cookies

Remix v0.18 also adds support for multiple Set-Cookie headers on a single response. If you were trying to do this before, this will be a welcome upgrade.

Aaaaaaand, that's it! We hope you enjoy using this release of Remix as much as we've enjoyed making it for you.

v0.17.0

17 Sep 21:09
Compare
Choose a tag to compare

This release brings us a giant step closer to v1 stable by introducing a new remix package that you'll use in all your app code. It also removes the old compiler from @remix-run/dev and completely replaces it with the new one we introduced in v0.15. Finally, we introduced a new <LiveReload> element that replaces our useLiveReload() hook.

There are a few breaking changes in this release, so we have bumped the minor version accordingly. This will likely be the last release of the 0.x series before we move to 1.x beta releases.

New remix Package

The major new feature in this release is the new remix package, which is a significant improvement in the way you use Remix in your app code. We recommend using the remix package for all your imports instead of importing directly from @remix-run/node and/or @remix-run/react. The remix package contains all the exports you need from both of those packages.

// You can replace these:
// import type { LoaderFunction } from "@remix-run/node";
// import { useRouteData } from "@remix-run/react";
// With this:
import type { LoaderFunction } from "remix";
import { useRouteData } from "remix";

export let loader: LoaderFunction = () => {
  return { now: Date.now() }
};

export default function HomePage() {
  let { now } = useRouteData();
  return (
    <p>This page was rendered at {new Date(now).toLocaleString()}.</p>
  );
}

Now you don't have to remember which package to get stuff from, which was kind of a pain in the past 🤪

If you start a new app today using npm init remix, you will automatically get { "dependencies": { "remix": "*" } } in your package.json. If you're upgrading an existing app, you'll want to add the remix package to your package.json dependencies:

$ npm add remix@*

Streamlined CLI Commands

We had a proliferation of CLI commands when we introduced the new compiler in v0.15, and then again when we added our own built-in app server in v0.16. But in v0.17, it all comes into focus! 🧐

We now have 3 remix CLI commands:

  • remix build - Runs the compiler and generates the build. This uses our new esbuild-based compiler, and was previously remix build2.
  • remix dev - Runs the compiler in watch mode and boots the dev server for live reloading. This was previously remix run2.
  • remix run - Runs the built-in application server (requires @remix-run/serve) + remix dev. This was previously remix run3.

These commands are designed to provide the right level of functionality in several different usage scenarios. The story goes something like this:

  • If you're using remix-serve to deploy your app in production, use remix run in development. It's the same server plus everything that remix dev does.
  • If you're using Architect/Vercel/Firebase or @remix-run/express in your own node server.js, use remix dev in development. You'll have to run 2 processes either in 2 separate terminal tabs or using a process manager like pm2-dev.

We are very happy to finally have some resolution here. It got crazy there for a second 😅

New <LiveReload> Element

This release introduces a new <LiveReload> element that replaces the useLiveReload() hook we shipped in v0.16. Having an element instead of a hook is a little more ergonomic since the rules of hooks require you to always use them, but you aren't always in dev. The <LiveReload> element also does not require you to render a <Scripts> element or hydrate the page, so it works when you are developing a page without any other scripts.

If you were using useLiveReload() previously, replace it with a <LiveReload> element in the same component.

import { LiveReload } from "remix";

export default function MyApp() {
  // Instead of this:
  // useLiveReload();

  return (
    <Document>
      <p>Welcome to the app!</p>

      {/* Use this: */}
      {process.env.NODE_ENV === "development" && <LiveReload />}
    </Document>
  )
}

We hope you enjoy this release as much as we've enjoyed making it for you. Onward to 1.0!

v0.16.0

17 Sep 21:13
Compare
Choose a tag to compare

This release gives a taste of what it will be like after our 1.0 release: no breaking changes just improvements and bug fixes 😎

New Features

*.server.js and *.client.js are back!

The new esbuild-based compiler now supports the *.server.js and *.client.js extensions to exclude modules from the opposite build. *.server.js files will only be included in the server bundle and *.client.js files will only be included in the browser bundles.

This allows you to have module side-effects like reading window in client only files:

// ./stripe.client.js
let stripe = createStripe(window.ENV.STRIPE_PUBLIC_KEY);
export { stripe };

Before that would have thrown an error while server rendering, but now it will be excluded from the server build.

Or if you've got node_modules that are doing dynamic requires or have module side-effects that are messing up your browser bundles, you can just eliminate them from the browser bundles by putting those imports into a *.server.js file.

// ./problematic-thing.server.js
import { stuff } from "problematic-thing";
export { stuff };

Now that file will be excluded from client bundles and your problems go away.

npm init remix

Goodbye starters, hello generator!

Instead of cloning starter repos and fiddling around with an .npmrc, we have a new project generator. Give it shot! open up a terminal and type:

npm init remix

It will ask you where you want to deploy, will auto-detect your Remix token in a home ~/.npmrc or REMIX_TOKEN environment variable, ask if you want TypeScript or JavaScript, and then generate the project for you.

remix-serve

If you already tried out npm init remix you may have noticed the first server option was "Remix App Server".

We've been saying the whole time that "Remix isn't your server, it's just a function on your server". Now Remix can also be your server.

npm i @remix-run/serve
remix-serve <server-build-dir>

This is a small, but production-ready, express server for your Remix app. Not only is it great for production, but it makes starting a new project when inspiration strikes frictionless. Check it out:

  1. You get an idea!
  2. npm init remix
  3. npm run dev

And then you can deploy that app easily to places that run node apps like Heroku, Fly, Render, Google App Engine, AWS, etc.

remix dev and remix run3

Yep ... run3. 🤣 We'll explain in a second, but first here's what it does.

Now that we have a Remix App Server, we can have a command that runs both the app server and the dev asset server in the same process (no messing around with multiple tabs or concurrently/pm2-dev).

And that's exactly what run3 does for apps using the Remix App Server!

Okay, let us explain the name:

When we released the new esbuild-based compiler, we added run2 and build2. In the release after this we will be removing the old compiler. Then we can clean up this cli's hilarious command names.

In the next release there will only be three commands:

  • remix build - builds your app for production (today's build2)
  • remix dev - starts the asset dev server and rebuilds on file changes (today's run2)
  • remix run - runs app and dev asset server in the same process in develoment mode (today's run3)

You can use remix dev today instead of run2.

useLiveReload

Running remix dev now supports live reload! When you change files the browser will automatically reload the page. This is a big productivity enhancement.

You have complete control over using this feature or not:

import { useLiveReload } from "remix";

export function Root() {
  useLiveReload();
  return <html>{/* ... */}</html>;
}

It will only connect to the dev server in development mode, so you don't have to worry about removing the hook's call site in production.

People are curious about HMR after this. It will likely happen but not anytime soon. HMR in React is heavily dependent on babel which our new compiler doesn't use. We've explored it and know the amount of effort it will take, so we'll be waiting until our business can afford the investment in a marginal improvement over live reload.

Fixes

useSubmit now works as expected when not passing in a form or button element.

v0.15.0

17 Sep 21:14
Compare
Choose a tag to compare

This release introduces an experimental new compiler based on esbuild, which is much faster than Rollup. In our tests, we've found the new compiler to be anywhere from 20-100x faster than the current compiler.

To help with migrating to the new compiler, this release ships with both our current compiler and the new one. So you can upgrade your app to Remix 0.15 and run it on the current compiler, then try out the new compiler and see what breaks. We think you'll enjoy the speed improvements so much that you'll want to switch as soon as possible. The time you spend making the migration will easily be worth the time you won't spend waiting on future builds 😅

There are also a few minor changes to a few of our packages that are going to help us get onto more platforms in the future. Right now Remix runs only on node. But as more JavaScript runtimes appear on different cloud providers (hello Cloudflare Workers!) we are going to run Remix on all of them.

Upgrading Summary

To upgrade from Remix 0.14:

  • Replace @remix-run/data with @remix-run/node in your package.json dependencies
    • npm remove @remix-run/data
    • npm add @remix-run/node
  • Change all @remix-run/data imports to @remix-run/node
  • Remove deep imports from @remix-run/react:
    • In app/entry.client.js, change import Remix from "@remix-run/react/browser" to import { RemixBrowser as Remix } from "remix"
    • In app/entry.server.js, change import Remix from "@remix-run/react/server" to import { RemixServer as Remix } from "remix"

Please see the "Notes on Package Changes" section below for background on these changes.

As always, remember to use the starters as your guide. We currently have starter repos for:

Using the New Compiler

Note: Before you attempt this, please be sure you follow the notes in the Upgrading Summary section (above) first! Once you get your app working with the current compiler, come back here and try to use the new one.

This release ships with a new experimental compiler based on esbuild. We have been blown away by how fast esbuild is, and we think you're going to really enjoy the speed improvements too. However, there are some changes you'll need to make to your app in order to use it.

The first thing you'll need to change is the CLI command you use to invoke the compiler. In this release, remix build will still invoke the old compiler and remix build2 will invoke the new one. Eventually remix build2 will become remix build and we'll remove the old compiler entirely. This is the same for remix run and remix run2. You can also use remix watch2 if you just want to run the build in watch mode without firing up the dev server.

The new compiler does not support:

  • .client.js and .server.js file extensions
  • url:, css:, and img: imports
  • .mdx route modules

I know that's a lot to drop on you in a single release, but that's why we're calling this compiler "experimental"! Please allow me to explain in greater detail how we are thinking about each of these features.

Client/server-only Modules

In v0.13 we introduced the .client.js and .server.js file naming scheme for manually hinting to the compiler which files to include in the build. We used these hints as a way to try and speed up the compiler by giving hints to Rollup about which files it needed to include in the build, but esbuild is so fast that we don't need them anymore.

We still rely on tree-shaking to get rid of server-only code in your client bundles, so that hasn't changed. The main difference is that esbuild uses the sideEffects: false flag in package.json to determine whether a module has side effects or not. This is a webpack-ism that has been around for a few years now, so it should already be supported in all the packages you need in your app. However, you will need to add that field to your app's package.json in order for esbuild to eliminate your server-only modules from the client bundles.

Add this to your app's package.json:

{
  "sideEffects": false
}

If you have browser-only code, instead of using a .client.js file extension, you can make sure it won't run on the server using a traditional typeof window guard:

if (typeof window !== "undefined") {
  // browser-only stuff goes here
}

URL, CSS, and Image Imports

The new compiler includes support for importing many different types of files as URLs including SVG, fonts, CSS, and images. When you import a file, it will be copied to the build output and you will get a cache-able (hashed) URL to the asset.

import logoImageUrl from "../images/logo.png";

function Logo() {
  return <img src={logoImageUrl} />;
}

We are still working out how we'd like to support CSS and images. esbuild includes several different loaders for different content types, and they are working on native support for CSS and CSS modules, so we are following that work closely to see how it pans out.

As for images, we are still working out how we can best support all the different sources of images without slowing down the compiler. There are many different sources for images, but the img: import strategy only really works for image files that are stored alongside your source code. That won't be the case if you have a lot of images or if they are generated by users. So we are rethinking how to best handle these in the new compiler. There are also many different ways to serve images. Services like Akamai and Cloudinary are popular choices for hosting images, and they make it really easy to generate different formats. CDNs like Fastly and Cloudflare also have image optimization capabilities, and it's also very compelling to be able to build them on the fly as needed instead of building it into the build step of your app.

For now, when you import a .css or image file using the new compiler, you'll get a hashed URL to that file. We will also be writing up some detailed guides about the various strategies for handling CSS and images in Remix apps using the new compiler. The guide to using PostCSS in Remix is already a good start.

MDX Route Modules

The new compiler does not support using .mdx files as route modules. We are planning on re-introducting first-class support for .mdx files as route modules as soon as possible.

In the mean time, if you're using MDX one project you might be interested in is Kent C. Dodds' mdx-bundler. It's a fast tool (also based on esbuild 🙌) that will bundle up your MDX for you and give you the code you need to actually render your component. You could possibly move your MDX out of the app/routes directory and into some other directory like app/pages. The new compiler will give you the text of the file (instead of a URL) when you import text from "../pages/something.mdx", so you could do something like this:

import { bundleMDX } from "mdx-bundler";
import { getMDXComponent } from "mdx-bundler/client";
import { useRouteData } from "remix";

// You can get the MDX from your own filesystem. But if you have a lot
// of content you're probably going to get this from a database somewhere.
import text from "../pages/something.mdx";

export async function loader() {
  // You could load MDX from a database here!
  // let text = await getText("https://github.com/my/repo/posts/something.mdx");
  return await bundleMDX(text);
}

export default function MyPage() {
  let { code } = useRouteData();
  let Component = React.useMemo(() => getMDXComponent(code), [code]);
  return <Component />;
}

We believe this is a great way to handle MDX; as content instead of source code. Now you don't have to slow down your build compiling a bunch of MDX files!

Done!

Aaaaaaand that should be it if you'd like to try out the new compiler. Please let us know if we missed something so we can add it to these notes. And please let us know how it goes so we can continue to improve the new compiler!

Notes on Package Changes

In the last release (0.14) and this one some packages have moved around, and I thought it'd be nice to put a few notes here about how we are thinking about structuring our packages going forward so it's clear why we are making these changes. Hopefully this will clarify how we are thinking about supporting Cloudflare Workers in the near future as well.

We currently ship three separate packages that run on node: @remix-run/architect, @remix-run/express, and @remix-run/vercel. Each package shares a common dependency, @remix-run/node, and exports a createRequestHandler function that is suited for working with that particular provider's HTTP server API.

In this release, we eliminated the @remix-run/data package and elevated @remix-run/node to an app-level dependency (in 0.14 it was a transitive dependency called @remix-run/core) to more accurately reflect its target runtime. So if you're building a node app with Remix, your app-level dependencies are:

  • @remix-run/node (the "environment" you're running in)
  • Your "provider" (currently @remix-run/architect, @remix-run/express, or @remix-run/vercel)
  • @remix-run/react

We will follow this same pattern to support other JS runtimes in the near future. So e.g. if you're running your app in Cloudflare Workers, the packages you'll need will be (names may change):

  • @remix-run/service-worker
  • @remix-run/cloudflare-workers
  • @remix-run/react

Hopefully that clarifies how we are thinking about structuring dependencies going forward! Thanks for your patience as we work this out.

v0.14.0

17 Sep 21:14
Compare
Choose a tag to compare

This release includes some significant improvements to the way Remix apps are deployed to production. It includes two major improvements to a production Remix app:

  • No more dev-only dependencies being deployed to your server
  • No more dynamic requires

These are some pretty major changes to the underlying architecture of Remix that fixes support for several cloud providers and opens the door to deploying to many more.

Specifially, this release fixes deployments on both Architect (broken in 0.10) and Vercel (who recently changed the way they do deploys).

Upgrading Summary

To upgrade from Remix 0.13:

  • Remove @remix-run/cli from your dependencies in package.json and replace it with @remix-run/dev in your devDependencies
  • Use createRequestHandler({ build: require("./build") }) in server.js
  • Add a postinstall step to run remix build
  • In dev mode, watch build/assets.json to know when to restart the server

As always, remember to use the starters as your guide. We currently have starter repos for:

Background

Before Remix 0.14, we had both development and production dependencies in the same package: @remix-run/core. Having everything in one package helped us bootstrap the project quickly, but also became a burden over time. For example, as we added features to the Remix compiler, which is only needed in development, the overall size of the dependencies Remix needed in a production deployment grew. So, we knew we needed to split out all of Remix's development dependencies into a separate package. In addition, some of our dev dependencies require binaries that we are not able to deploy to AWS. Separating them out into a dev-only package fixes production deploys on AWS.

Another issue with deploying Remix to production before this release is that it required you to deploy your remix.config.js file (and your app directory!) alongside your build directory. Remix would read the config and reconstruct the route hierarchy at runtime so that it knew about any dynamic routes you created using config.routes. It would also dynamically require all modules needed to run your app, which was a non-starter on hosts like Netlify (and, suddenly as of this week, Vercel 🤷‍♂️). We knew we needed to get rid of all dynamic requires in production. You shouldn't have to deploy your source code, just the build artifacts.

So ... this release splits up our core dependency and completely alters the way we load modules and deploy to production.

No sweat, right? 😅

The big winner is that your production deploys are going to be more streamlined (no dev dependencies) and we are laying the groundwork for being able to deploy Remix to more providers by eliminating dynamic requires.

Upgrading

Despite the significant changes on our side, upgrading your app from Remix 0.13 should be fairly straightforward.

First, remove @remix-run/cli from your dependencies in package.json and replace it with @remix-run/dev in your devDependencies.

$ npm remove @remix-run/cli
$ npm add -D @remix-run/dev

Going forward, @remix-run/dev will contain everything you need to develop a Remix app. It should only ever be in devDependencies so it doesn't end up in your production environment. We even named it dev to help you remember :D

Next, we need to let the Remix server know about our app. In Remix 0.14, your entire app is compiled down to a single module in build/index.js with static require calls to load the rest of the app. This means you can load the entire app using require("./build") (or whatever your config.serverBuildDirectory is).

To upgrade, open up server.js (or your serverless function module) and add the build key to your server's createRequestHandler:

createRequestHandler({
  // Add this line!
  build: require("./build"),
  getLoadContext() {
    // ...
  }
});

Note: You can see how we did it in the Express starter.

This line requires the entire app and passes it to your server. Of course, this means you'll need to actually build the app before you can start the server, so you may want to add a postinstall hook to your package.json so that your app is ready to go after a fresh npm install:

{
  "scripts": {
    "postinstall": "remix build"
  }
}

If you're using Vercel, Architect, or Firebase, this postintall hook isn't necessary, just make sure your build finishes before you open the app in the browser.

In Express apps, you'll probably also want to update your file watcher in development. We currently recommend watching build/assets.json to know when to restart your server.

In the Express starter we use PM2. The relevant portion of the config looks like this:

module.exports = {
  apps: [
    {
      script: "server.js",
      watch: ["build/assets.json"]
    }
  ]
};

Note: There are actually 2 builds going on (server and client), but the client build always takes longer since it has to bundle dependencies as well as your app code. So that's why we recommend watching build/assets.json. We are, however, working on improving our build times, so this recommendation may change soon.

If you're using Vercel or Architect, you don't need to set up any watchers, these providers dev servers already handle the file watching.

Vercel Notes

The build config and remix config in Vercel deploys were a bit involved because of our dynamic requires, you can now greatly simplify it, and even use Vercel's auto-deploys from GitHub now.

Your remix.config.js no longer needs to branch on any environment variables, it can be simple again:

module.exports = {
  appDirectory: "app",
  browserBuildDirectory: "public/build",
  publicPath: "/build/",
  serverBuildDirectory: "build",
  devServerPort: 8002
};

Your vercel.json can likewise be simplified by removing the includeFiles option in your builds config:

{
  "builds": [
    {
      "src": "index.js",
      "use": "@vercel/node"
    }
  ],
  // etc.
}

Because your app modules are all in the require graph now, we don't need to provide any hints to Vercel about what to deploy, it knows just by looking at your index file.

Other Changes

A few other miscellaneous changes in this release that you may be interested in:

  • The compiler now uses an on-disk cache that defaults to the .cache directory in the root (sibling to remix.config.js). If you want to put it somewhere else, use config.cacheDirectory in your remix.config.js
  • The compiler doesn't replace any process.env.NODE_ENV strings in your server code anymore, so you should get that value from the actual process that is running your server

Enjoy your slimmed down production builds!

v0.13.0

17 Sep 21:15
Compare
Choose a tag to compare

Lots of bug fixes, some new features, and we ALMOST made it w/o a breaking change, but there is one, it's super easy though.

New Entry File Names

This is the only thing you have to do to upgrade from v0.12.x:

  • Rename app/entry-browser.js to app/entry.client.js (or .tsx)
  • Rename app/entry-server.js to app/entry.server.js (or .tsx)

This brings our file naming conventions in alignment with one of the new features in this release.

Excluding modules from the client and server bundles

We haven't talked about this very much publicly, but generally speaking the Remix compiler does a decent job at deciding which modules to include in your browser bundles vs. which are meant only for the server. It does this through a feature known as "tree-shaking" that permits the compiler to remove dead code from the output bundles.

Let's say you have a module that contains a few functions for accessing your backend database. You could import this module into one of your route modules so you can use it in your loader and/or action, like this:

import { useRouteData } from "remix";
import { json } from "@remix-run/data";

import { db } from "../database";

export async function loader({ params }) {
  let user = await db.select("users", (where: { userId: params.userId }));
  return json({ user });
}

export function MyPage() {
  let { user } = useRouteData();
  // ...
}

At compile time, we can see that the only place you're using anything from ../database is in your loader, so when we build the client bundle we can remove that code entirely ("tree-shake" it) from the build. This includes both the loader function itself, as well as the import of ../database!

This works great most of the time, but sometimes you get into weird situations where the compiler can't automatically infer which files it needs only on the server, or only on the client. For these times, we provide an escape hatch: *.client.js and *.server.js.

For example, on our own website we got into a situation where we were importing both firebase and firebase-admin in our /login route. We use the firebase package in the component code to create the user session, and we use the firebase-admin package in the loader (on the server) to verify and create the cookie. Our code looked something like this:

import admin from "../utils/firebaseAdmin.js";
import firebase from "../utils/firebase.js";

export function loader() {
  // use `admin` in here
}

export function LoginPage() {
  function loginFormHandler() {
    // use `firebase` in here
  }

  // ...
}

The firebase package isn't really meant to run on the server--it's client-only. But we can't easily infer that it's not needed in the server bundles because of the way it's used in an event handler. So instead, we use the .client.js file extension on our utils/firebase.js to exclude it from the server build!

All we need to do is change our filename:

import firebase from "../utils/firebase.client.js";

Now utils/firebase.client.js won't ever end up in the server bundles.

So that's the feature in a nutshell: use .server.js (or .server.tsx) as your file extension when you know a file is only ever meant to be run server-side, or use .client.js when it's only ever meant to run in the browser. And remember, most of the time the compiler should automatically be able to figure it out for you, so this is really just an escape hatch!

CSS Imports

You can now import CSS with the css: import assertion. It's just like url: except that the file will be processed with PostCSS (as long as you have a postcss.config.js file in the Remix app root).

// <app root>/postcss.config.js
module.exports = {
  plugins: [require("autoprefixer"), require("cssnano")]
};
// <app root>/routes/some-route.js
import style from "css:../styles/something.css";

// usually used with links
export let links = () => {
  return [{ rel: "stylesheet", href: style }];
};

You can find a few PostCSS setups in the styling docs.

Note: Using this plugin will slow down your builds. Remix won't rebuild a file that hasn't changed, even between restarts as long as you haven't deleted your browser build directory. It's usually not a big deal unless you're using tailwind where it's common for 5-20 seconds to build a file the first time depending on your tailwind config.

useMatches hook and Route Module handle export

Remix internally knows the all of the routes that match at the very top of the application hierachy even though routes down deeper fetched the data. It's how <Meta />, <Links />, and <Scripts /> elements know what to render.

This new hook allows you to create similar conventions, giving you access to all of the route matches and their data on the current page.

This is useful for creating things like data-driven breadcrumbs or any other kind of app convention. Before you can do that, you need a way for your route to export an api, or a "handle". Check out how we can create breadcrumbs in root.tsx.

First, your routes can put whatever they want on the handle, here we use breadcrumb, it's not a Remix thing, it's whatever you want.

// routes/some-route.tsx
export let handle = {
  breadcrumb: () => <Link to="/some-route">Some Route</Link>
};
// routes/some-route/some-child-route.tsx
export let handle = {
  breadcrumb: () => <Link to="/some-route/some-child-route">Child Route</Link>
};

And then we can use this in our root route:

import { Links, Scripts, useRouteData, useMatches } from "remix";

export default function Root() {
  let matches = useMatches();

  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <Links />
      </head>
      <body>
        <header>
          <ol>
            {matches
              // skip routes that don't have a breadcrumb
              .filter(match => match.handle && match.handle.breadcrumb)
              // render breadcrumbs!
              .map((match, index) => (
                <li key={index}>{match.handle.breadcrumb(match)}</li>
              ))}
          </ol>
        </header>

        <Outlet />
      </body>
    </html>
  );
}

A match looks like:

interface {
  // The amount of the URL this route matched
  pathname: string;

  // whatever your route's loader returned
  data: any;

  // the parsed params from the url
  params: { [name: string]: string };

  // the handle exported from your route module
  handle: any;
}

We're excited to see what conventions you come up with!

Everything else

  • Added action to usePendingFormSubmit()
  • Fixed 404 pages
  • Fixed using non-HTML elements (e.g. FormData, URLSearchParams) with useSubmit
  • Fixed using Open Graph tags with route meta function

Enjoy!

v0.12.0

17 Sep 21:15
Compare
Choose a tag to compare

Automatic Performance Optimizations

In the 90's we were told to not go chasing waterfalls. It's great advice for modern web development also.

A request waterfall happens when fetching one resource leads to fetching another resource. If a website imports your root route, and your root route imports React, you have a waterfall. The browser doesn't know you want React until it has already download the root route. If we could fetch both modules in parallel, we'd drastically reduce the amount of time it takes to download all the resources we need.

Remix now uses <link rel="modulepreload"> on all the scripts the page needs, automatically, to prevent these waterfalls. For example, in one of our demo apps we're building, the bottom of the document now has these links.

<link rel="modulepreload" href="/build/_shared/react-2daf095e.js" />
<link rel="modulepreload" href="/build/_shared/react-dom-1e9b93b6.js" />
<link rel="modulepreload" href="/build/_shared/__remix-run/react-624064ed.js" />
<link rel="modulepreload" href="/build/_shared/object-assign-510802f4.js" />
<link rel="modulepreload" href="/build/_shared/scheduler-5591ac82.js" />
<link rel="modulepreload" href="/build/_shared/history-e6417d88.js" />
<link rel="modulepreload" href="/build/_shared/__babel/runtime-88c72f87.js" />
<link rel="modulepreload" href="/build/_shared/react-router-4449037e.js" />
<link rel="modulepreload" href="/build/root-4ac7c97d.js" />
<link rel="modulepreload" href="/build/routes/login-538f9d25.js" />

Previously, we only imported "root-4ac7c97d.js" and "login-538f9d25.js", and then an import waterfall for everything else began: root downloaded and required react, react downloaded and required scheduler, scheduler downloaded and required object assign, etc.

Now, the browser has already started downloading all of the dependencies for the entire page in parallel, greatly reducing the amount of time it takes for the page to hydrate.

Additionally, on script transitions (transitions with <Link> not <a href>), Remix will likewise modulepreload all of the scripts for the next page in parallel with the data. In typical React apps, you fetch data, render, fetch code-split bundles, render, fetch data, render, fetch code split bundles, render, etc. Causing even more waterfalls. Because of nested routes, we can download all of the bundles and all of the data for all of the matching routes in parallel. This prevents both render/fetch waterfalls as well as module import waterfalls!

url: imports

Like last release's image imports, you can now import any file you want, Remix will emit that asset to your browser build directory and return the url to your app to use anywhere.

For example, you can import css like this:

import stylesUrl from "url:./styles/some-style.css";

// `stylesUrl` will be a string like "/public/build/styles/something-2ac45cffe9.css"

You can import any kind of file you want, Remix will simply emit the asset and give it a fingerprint in production based on the content of the file to make it easier to cache.

Keep reading to see where you'll likely use this.

Route Module Links

Like a route module meta export, you can now export a links function that tells Remix which links to add to the document when this route is active, and which to remove when it's not.

In your root route, render the <Links> element by the <Meta> element, and then in any route export a links function and Remix will put them in the document.

import type { LinksFunction } from "remix";
import { Links, Meta, Scripts } from "remix";
import { Outlet } from "react-router-dom";

// New!
export let links: LinksFunction = () => {
  return [{ rel: "icon", href: "/favicon.png" }];
};

export default function Root() {
  return (
    <html>
      <head>
        <Meta />
        <Links /> {/* <-- New! */}
      </head>
      <body>
        <Outlet />
        <Scripts />
      </body>
    </html>
  );
}

These links become <link> tags, so any properties on your object become properties on the element.

You can use it along with the recent image import feature:

import favicon from "img:./favicon.png?width=32&height=32";

export let links: LinksFunction = () => {
  return [
    {
      rel: "icon",
      href: favicon.src,
      type: `image/${favicon.format}`
    }
  ];
};

The links function receives the route's data so you can dynamically prefetch stuff, we'll see an example later, but the signature is simple:

export let links: LinksFunction = ({ data }) => {
  // `data` is your route loader's data
};

There is a LOT you can do with the links export, make sure to read the links docs after these release notes.

BREAKING: No more CSS convention, use links

CSS files in the routes folder are no longer included automatically, we've got something better.

Instead of cluttering up your routes folder with css files, we can use the url: imports and links instead.

import type { LinksFunction } from "remix";
import styles from "url:../styles/login.css";

export let links: LinksFunction = () => {
  return [{ rel: "stylesheet", href: styles }];
};

This gives you more control over what css ends up on the page, and allows you to re-use css across unrelated routes. (It also sets us up for adding css preprocessing features directly into remix with a css: import assertion coming soon.)

For example, what if you wanted to load different css for a route depending on the device size?

import type { LinksFunction } from "remix";
import mobile from "url:../styles/login-mobile.css";
import desktop from "url:../styles/login-desktop.css";

export let links: LinksFunction = () => {
  return [
    { rel: "stylesheet", href: mobile, media: "(max-width: 764px)" },
    { rel: "stylesheet", href: desktop, media: "(min-width: 765px)" }
  ];
};

You don't even need the media queries in your CSS, only ship the styles they're going to use! This was simply not possible with conventional route CSS files.

Prefetching other pages for faster transitions

Some React frameworks automatically download all the JavaScript--and in the case of SSG, all of the data pre-rendered inside the JavaScript--for every link on the page. This is why when you go to a very boring page built with one of these tools your network tab shows 7 megabytes of JavaScript on the page. It downloaded half of the entire website.

We don't want to do that to your user's data plan but we do think it's a great strategy to speed up links to pages the user is likely to visit next.

What do we mean by "likely"? Some examples:

  • User is on the login page, it's likely they'll end up at the dashboard, so prefetch the resources for the dashboard.
  • User is on the shopping cart page, it's likely they'll end up on the checkout page next.
  • User is on an index page of a list of invoices, it's likely they'll end up on an invoice page.

So instead of downloading the code for 20 pages in your footer, you get to choose which transitions you'd like to optimize.

Browsers have a built-in way to prefetch resources, and prime the browser cache, before you use them: <link>. The way to prefetch a page in Remix is the new links export with a special kind of link descriptor (that's what we call them):

Prefetching a page's resources

Let's take the login → dashboard example:

import type { LinksFunction } from "remix";
import styles from "url:../styles/login.css";

export let links: LinksFunction = () => {
  return [
    { rel: "stylesheet", href: mobile, media: "(max-width: 764px)" },
    { page: "/dashboard" } // <-- that's it!
  ];
};

By using page on your link descriptor, Remix will match that route and explode your little {page: "/dashboard" } descriptor into all of the <link rel="prefetch" as="script" href={asset} /> tags the user will need when they get to the dashboard. On one of our demo apps it turns into 24 <link> tags!

These kinds of optimizations would be really hard to do by hand, we're pumped we've got all the right pieces to do it for you.

You can even prefetch the data for the next page with { page: "/users/123", data: true }. You'll prefetch all the JavaScript assets as well as all the loader data for that page. In addition to the script prefetches, Remix will add <link rel="prefetch" as="fetch" href={dataLoaderUrl} /> for any of the routes will need to fetch data. This primes the browser cache so that when the user clicks, the browser can read from the cache. And since it's just using browser features, if the user clicks the link long after the cache headers on the prefetch have expired then the browser will refetch. Nothing special about Remix, that's just how prefetch links work!

Be careful with this feature. You don't want to download 10MB of JavaScript and data for page the user probably won't ever visit.

Blocking Transitions on Preloads

You may have heard of FOUC (flash of unstyled content) and CLS (cumulative layout shift). This happens any time the user visits a page and the styles swap around or the layout shifts all over the place. We'll just call both concepts "jank" for now.

Browsers, by default, wait for the CSS for a page to download before they render the document to prevent jank. But when it comes to script transitions (with client side routing) it's easy to introduce jank because the browser can't know which resources to block and which not to.

With Remix, you can specify which preloaded resources for a page should block a script transition before rendering the next page. Check it out:

import type { LinksFunction } from "remix";
import { block...
Read more