Skip to content

Releases: remix-run/remix

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

v0.11.0

17 Sep 21:19
Compare
Choose a tag to compare

Sessions and Cookies

This release introduces first-class support for sessions and cookies at the route level instead of piggy-backing on framework middleware like express-session.

Background

We initially thought we could just build out our sessions API on top of existing frameworks and cloud providers. After all, why reinvent something that is already working well for people? One of the core goals of Remix is to play well with your existing stack and technologies that you already know and love.

However, we quickly discovered that not all cloud providers support session management. To some of them it's a detail that's best left up to the app developer. That's a fair position, but it means that in order to provide a consistent experience in Remix when it comes to handling sessions, we had a bit of work to do.

Another reason we weren't comfortable with this approach is that in many node server frameworks, sessions rely on using middleware. To use sessions in Express, you insert some middleware into your stack in one spot and it runs for multiple different request handlers.

While there's nothing inherently bad about this model, our goal with Remix is to push all functionality for handling a request into your route loader and action functions. This means each one will have a bit more work to do, but it also means it's completely isolated from other request handlers in your app, which is closer to the lambda model. Our hope is that by enforcing an architecture that completely encapsulates the request/response cycle at the route level it will be easier to eventually deploy Remix loaders and actions to individual cloud functions, if desired.

That being said, remember your escape hatch is always getLoadContext. So if you really want to use something from your cloud provider or framework (like an Express middleware) and get it through to your loaders/actions, we completely support that.

Session storage

The major new API in this release is the SessionStorage interface. A session storage object is responsible for parsing/serializing HTTP cookies and managing persistence of session data between requests.

SessionStorage has three methods:

  • getSession - Returns the Session object associated with a HTTP cookie, or a new session if there was no Cookie header
  • commitSession - Saves session data to storage and returns the value to use for the response's Set-Cookie header
  • destroySession - Like commitSession, but destroys all session data instead of saving it

Remix provides three built-in session storage options:

  • createFileSessionStorage() - Stores session data on the filesystem, ala PHP
  • createCookieSessionStorage() - Stores session data in the session cookie itself. This is useful when you don't have a filesystem or don't want to use a database for some reason.
  • createMemorySessionStorage() - A lightweight development/testing-only session storage for testing your session-based user flows

The docs on sessions contain a comprehensive example of how you might build a simple user authentication form using the new API.

The docs also include an example of building a custom session storage using the new createSessionStorage() API, which is designed to let you build a session storage solution backed by any database you want.

Cookies

Of course, a core component behind sessions is the ability to handle HTTP cookies. Remix v0.11 also includes a Cookie interface and createCookie() function that help when dealing with cookies generally.

The idea behind the Cookie API is that you create reusable cookie objects that know how to parse and serialize cookies, complete with support for rotating secrets used for signing cookies to verify their integrity (important when it's a session cookie).

Changes to server endpoints

This is mostly an internal change, but in order to make cookies work better in your loaders and actions, we've made a few subtle changes to the server endpoints. Specifically, the /_remix/data endpoint is now just the regular route endpoint with a ?_data parameter in the query string. This means when you set a path on your cookies to the path of one of your routes, you'll get that cookie in your loader/action as well.

The /_remix/manifest endpoint also changed to the same as the route endpoint, but with a ?_manifest parameter in the URL query string. This is for consistency with the data endpoint, but may also prove helpful in the future for specifying additional security around the manifest endpoint.

In addition to these usability benefits, it tends to clean up the Network tab quite a bit as well :D

Upgrading

Upgrading to v0.10 should be zero work if you weren't using sessions. If you were, take the following steps:

  • Create your own session storage object in app/sessionStorage.ts (or app/sessionStorage.js if you're using JavaScript)
  • Remove the session argument in your loader and action functions and use the getSession and commitSession functions from your session storage object instead
  • For @remix-run/express - Remove your express session middleware since we're not using it anymore

And that should be just about it. Please let us know if you encounter any issues upgrading.

As always, thank you for your support!

v0.10.0

17 Sep 21:19
Compare
Choose a tag to compare

Release Overview on YouTube

Root Layout Route

Background

The App.tsx file has been a little bit goofy. This release cleans that up. Instead of needing to import the app file into both entry-server and entry-client, we've now got the concept of a "root route". This root route works like all others, it can export a loader and error boundary, etc.

This small change removes a few APIs and a lot of complexity in Remix's internals. It was one of those -200 line changes without losing any features, which always feels amazing.

Upgrading:

Move App.tsx to root.tsx

Move your App.tsx to root.tsx. "root" has special meaning, so it must be named that.

Change <Routes/> to <Outlet/>

Now that root.tsx is a route, you render an Outlet just like every other layout route.

// OLD
import { Routes } from "remix";

export default function App() {
  return (
    <html>
      {/* ... */}
      <body>
        <Routes />
      </body>
    </html>
  );
}

// NEW
// Note it's from "react-router-dom"!
import { Outlet } from "react-router-dom";

export default function App() {
  return (
    <html>
      {/* ... */}
      <body>
        <Outlet />
      </body>
    </html>
  );
}

Move global data code

Because root.tsx iss now a route it can export a loader for it's own data. If you had a global-data.ts file, cut and paste the code into root.tsx so it's co-located like all of your other routes.

Also, there is no longer a useGlobalData hook because this is now normal route data. So change your useGlobalData() to useRouteData().

Update your server and browser entries

Don't import or render App.tsx in entry-server or entry-browser anymore:

// OLD: entry-server
import App, { ErrorBoundary } from "./App";

let markup = ReactDOMServer.renderToString(
  <Remix context={remixContext} url={request.url} ErrorBoundary={ErrorBoundary}>
    <App />
  </Remix>
);

// NEW: entry-server
// - no more App import because you moved it to `root.tsx`
// - no more ErrorBoundary prop because the root route exports
//   it's own error boundary like any other route
let markup = ReactDOMServer.renderToString(
  <Remix context={remixContext} url={request.url} />
);

Do the same thing in entry-browser.tsx, it'll end up looking something like this:

import ReactDOM from "react-dom";
import Remix from "@remix-run/react/browser";

ReactDOM.hydrate(<Remix />, document);

Much nicer! No more special casing the root layout. We're also thinking this opens up the possibility for multiple root layouts: think signed-in-layout and signed-out-layout". We're not there yet though :)

Importing images

You can now import and resize images as a JavaScript module containing links to the assets, height and width attributes, and even responsive image source sets. Check it out!

// - change quality to 50
// - reformat to avif
// - generate a server rendered Base64 blurry placeholder
// - resize to 500
import guitar from "img:./guitar.jpg?quality=50&format=avif&placeholder&width=500";

// - change quality to 80
// - generate 3 responsive image sizes and a srcset for the `<img srcSet/>`
// - generate a ssr placeholder
import guitar2 from "img:./guitar.jpg?quality=80&srcset=720,1080,2048&placeholder";

export default function Guitar() {
  return (
    <div>
      <p>Fixed Image</p>
      <img
        alt="Guitar"
        src={guitar.src}
        style={{
          backgroundImage: `url(${guitar.placeholder})`,
          backgroundSize: "cover"
        }}
        width={guitar.width / 2}
        height={guitar.height / 2}
      />

      <p>Responsive</p>
      <img
        alt="Guitar"
        src={guitar2.src}
        srcSet={guitar2.srcset}
        style={{
          backgroundImage: `url(${guitar2.placeholder})`,
          backgroundSize: "cover"
        }}
      />
    </div>
  );
}

Go read the docs for more information!

v0.9.0

17 Sep 21:20
Compare
Choose a tag to compare

Release Overview on Youtube

This is another major change (like 0.8) that is focused on improving the developer experience in Remix. It's a major change because it essentially changes one of our core assumptions about how people would be using Remix based on feedback we've received since we launched our supporter preview in October.

tl;dr: In version 0.9, the data directory is gone (as is the dataDirectory export in remix.config.js). Instead, put your loader and action functions right there in your route modules (in app/routes) next to your component, headers, and meta. Remix will automatically compile builds for both the server (for server rendering) and the browser. data/global.js is now app/global-data.js.

Background

One of our main goals with Remix is that it doesn't have to own your entire stack. Sure, you could build an entire app on Remix. But if you have an existing node server, you don't have to abandon it or port everything to a new codebase when you decided to adopt Remix for your frontend. In line with this goal, Remix provides several different packages for working with various cloud providers including Architect (AWS Cloud Formation) and Vercel, and we are hard at work on many more.

We also assumed that, since Remix fits into your existing stack, we wouldn't have to handle compiling any of your backend code since you'd probably already have a build process in place. So we provided a data directory for all backend code. While it's technically possible to compile the backend code yourself, what this means in practice is that in order to use Remix you have to set up a separate build for data. And you probably don't already have a data directory because before Remix came along you didn't structure your code like that.

Additionally, many people are using TypeScript these days (we are!) and it's inconvenient to have separate folders for your data loaders and components when they use the same types! This caused a few of you to create a root-level types directory just so you could share code between data and app.

So, the assumption was that we didn't need to handle anything in the data directory, but based on your feedback we can see clearly this needs to change!

Improvements

As was mentioned previously, the data directory is gone in 0.9.Instead of putting your loader and action functions in data/routes, move those functions into the same corresponding files in app/routes alongside your route components, headers, and meta functions. If you had a data/global.js file, move it to app/global-data.js. Then go delete your data directory and your dataDirectory export in remix.config.js.

When remix run or remix build runs, Remix will generate two versions of your modules: one for the server (for server rendering) and one for the browser. For the browser build, Remix will automatically strip out any code that isn't meant to run in the browser. This means that server-only code in your loader and action functions (and any imports that are used only by them) won't appear anywhere in the browser bundles.

So if you had this in data/routes/team.ts:

import type { Loader } from "@remix-run/data";

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

export let loader: Loader = async () => {
  return await db.query("select * from team");
};

Go ahead and move that code into app/routes/team.tsx:

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

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

export let loader: Loader = async () => {
  return await db.query("select * from team");
};

interface TeamMember {
  name: string;
}

type Team = TeamMember[];

export default function MeetTheTeam() {
  let team = useRouteData<Team>();

  return (
    <div>
      <h1>Meet the Team</h1>
      <ul>
        {team.map(member => (
          <li>{member.name}</li>
        ))}
      </ul>
    </div>
  );
}

Now everything you need for a single route is in one spot; the data and the view. And don't forget you can always associate custom headers and/or meta information with a route in this file as well.

We've been using this already on our own projects internally and it feels really great to have everything in one spot. It's difficult to overstate the importance of avoiding context switching when working with code in order to move quickly and feel productive. One of the core innovations of React was keeping the state right there in the view, which made it feel incredibly productive almost immediately. We feel like this is a similar advantage of having everything in the same file in a route.

You might also feel like this makes it a little more tricky to think about what code in this file is going to run on the server and what code is going to run in the browser. But this isn't something new. Your components have always run on both the server and in the browser. That's just one of the trade-offs of server rendering! We are hoping that it will be easy enough to just remember that anything in loader and action won't make it to the browser.

Implementation Notes

You might be wondering how this all works behind the scenes, since any imports of server-only libraries like @prisma/client or firebase-admin aren't ever supposed to run in the browser.

To build this feature, we relied heavily on Rollup's built-in tree-shaking capabilities. When we run the browser build, we tell Rollup to ignore the loader and action methods in the output. We also tell it to ignore any module-level side effects, like database client initialization logic, so it aggressively prunes out the imports of any code in the module graph that is used only in loader and/or action.

Simpler TypeScript Setup

With the data directory, Remix wasn't compiling your TypeScript. This led to "two builds" in Remix. The application, not Remix, had to build TypeScript for the modules in your data directory, then Remix built TypeScript in your app. This made sharing code overly complicated and was just not as nice as having one build to worry about.

Because loaders/actions are inlined with your route modules, you no longer need a separate TypeScript build for data modules. If you used one of our starters, you can:

  • remove all of the TypeScript build and config from your app. In package.json you probably have some tsc -b and tsc -w code, you can remove it
  • get rid of all of your tsconfig.json files except app/tsconfig.js.

In the Overview Video, you can see all of the places affected by this. We're really happy with this change because it simplifies a lot of things for your application and for Remix itself.

Error Boundaries

We are also including first-class support for error boundaries in this release. Of course, you've always been able to use React 16's built-in error boundaries in Remix, but we have taken it one step further and introduced a top-level ErrorBoundary export in your route modules.

An ErrorBoundary is a component that is rendered in place of your route component whenever there is an error on that route, whether it be a render error (from inside one of your route components) or an error thrown from inside one of your loaders or actions.

In addition to supporting error boundaries at the route level, we also include support for a global ErrorBoundary as a prop to your <Remix> element. All of our starter repos have been updated to show how this is to be done (see app/entry-browser.tsx and app/entry-server.tsx).

Instead of using app/routes/500.tsx for your global error handler, Remix will now use your global ErrorBoundary component. It will still automatically change the HTTP response status code to 500. Since this functionality is only for real errors (uncaught exceptions), 500 is the appropriate status code.

Upgrading Summary

  • Move data/global.ts to app/global-data.ts
  • Delete the dataDirectory config in remix.config.js
  • Copy/paste the code in data/routes/<data-module>.ts to its corresponding app/routes/<route-module>.tsx file.
  • Delete your app/routes/500.tsx file
  • (optional) Add ErrorBoundary components to your routes and/or your top-level <Remix> element
  • If using one of our starter templates with TypeScript, remove all TypeScript build/config code except app/tsconfig.js

v0.8.0

17 Sep 21:20
Compare
Choose a tag to compare

This release is pretty significant and we're really excited about it. It sets the foundation for everything else we want to do with Remix. Hang on tight though, there are a lot of changes, and we appreciate your patience as we shift the API around a bit during this preview period. After our production release we'll have proper, backward-compatible API deprecation, but right now we're prioritizing getting Remix stable.

We recommend running through the tutorial with fresh eyes again to capture all of these changes.

Improvements

<Form> component and Actions

While previously "loaders" allowed you to load route data, "Actions" coupled with <Form> allow you to make changes to data with the simplicity of old-school forms posts but the progressive enhancement of React.

<Form method="post" action="/projects">
  <p>
    <label>
      Name: <input name="name" type="text" />
    </label>
  </p>
  <p>
    <label>
      Description: <textarea name="description" />
    </label>
  </p>
  <p>
    <button type="submit">Create</button>
  </p>
</Form>

And the action that handles the post:

import type { Action } from "@remix-run/data";
import { parseFormBody, redirect } from "@remix-run/data";

let action: Action = async ({ request }) => {
  let newProject = parseFormBody(request);
  let project = await createProject(newProject);
  return redirect(`/projects/${project.id}`);
};

export { action };

Finally, you can make the interaction fancy with usePendingFormSubmit() for loading indication and optimistic UI:

import { usePendingFormSubmit } from "remix";

function SomePage() {
  let pendingSubmit = usePendingFormSubmit();
  if (pendingSubmit) {
    return (
      <div style={{ opacity: 0.5 }}>{pendingSubmit.data.get("title")}</div>
    );
  } else {
    return (
      <Form>
        <input name="title" />
        <button type="submit">Create</button>
      </Form>
    );
  }
}

Read more:

Added usePendingLocation

This hook gives you the next location so you can match on its pathname for contextual loading indication on links and more

let nextLocation = usePendingLocation();
console.log(nextLocation && nextLocation.pathname);

Read More:

Added parseFormBody

Now that we have <Form> and Actions, you need a way to parse the form's request body.

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

let action = ({ request }) => {
  let body = parseFormBody(request);
};

It returns a URLSearchParams or FormBody depending on the encType of the form, both objects work almost identically though:

Read more:

Request object passed to loaders and actions

Instead of passing a URL, we pass the whole Request object so you can read the method, parse the body, etc.

let loader = ({ request }) => {
  request.method;
  request.url;

  let url = new URL(request.url);
  url.get("some-param");
  // etc.
};

Sessions

Remix platform wrappers like @remix-run/express can detect when you've enabled sessions for your app and automatically send a remix session object to loaders and actions. This is great for storing flash messages about actions that just happened on the server across your app or storing form validation errors to display on the next page.

let action = async ({ params, session }) => {
  let deletedProject = await archiveProject(params.projectId);
  session.flash(
    "globalMessage",
    `Project ${deletedProject.name} successfully archived`
  );
  return redirect("/dashboard");
};
// data/global.ts
let loader = ({ session }) => {
  let message = session.get("globalMessage") || null;
  return { message };
};
// app/App.tsx
export default function App() {
  let { message } = useGlobalData();
  return (
    <html>
      <head>
        <Meta />
        <Styles />
      </head>
      <body>
        {message && <div>{message}</div>}
        <Routes />
        <Scripts />
      </body>
    </html>
  );
}

Read More:

Importing .json files

You can now import .json files just like in Node.

import json from "./something.json";

console.log(json);

.ts and .tsx for routes/404 and routes/500

Previously they had to be .js.

Breaking Changes

Renamed @remix-run/loaders to @remix-run/data

Also, the remix config name for your loaders changed from loadersDirectory to dataDirectory.

While this is configurable, we also changed the default folder from "loaders" to "data" in the starter templates, and all docs now talk about the "data" folder instead of "loaders".

We made this change because your data loaders can now export two functions: loader and action. So it didn't make sense to call them "loaders" anymore but "data modules". So a "data module" can export a "loader" and and "action". Data modules live in data/routes/**/*{.js,.ts}.

In your data modules (previously "loaders"):

// old
import { json } from "@remix-run/loader";

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

In your remix.config.js

// old
exports.loadersDirectory = "./loaders";

// new
exports.dataDirectory = "./loaders";

// or if you want to be more semantic with the changes here, just make sure to
// rename the folder!
exports.dataDirectory = "./data";

Removed @remix-run/notFound

It wasn't that helpful and gave people the wrong idea.

// old:
import { notFound } from "@remix-run/loader";

module.exports = () => {
  return notFound();
};

// new:
exports.loader = () => {
  return new Response("", { status: 404 });
};

Please note that this does not render the routes/404 component, it renders whatever matched so you'll probably want to send some extra information down so your UI can handle it better.

// old:
import { notFound } from "@remix-run/loader";

module.exports = () => {
  return notFound();
};

// new:
import { json } from "@remix-run/loader";
exports.loader = () => {
  return json({ notFound: true }, { status: 404 });
};

Then you can read that data from useRouteData() and render a contextual not found page with the matching component.

Removed loader url

You can use the request object to create a url:

// old
let loader = ({ url }) => {
  let param = url.searchParams.get("foo");
};

// new
let loader = ({ request }) => {
  let url = new URL(request.url);
  let param = url.searchParams.get("foo");
};

Removed useLocationPending in favor of usePendingLocation

// old
let pending = useLocationPending();

// new
let nextLocation = usePendingLocation();

// or coerce to boolean and ensure identical behavior to your old code:
let nextLocation = usePendingLocation();
let pending = !!nextLocation;