v0.13.0 Release Notes

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">
        <meta charSet="utf-8" />
        <Links />
              // 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>

        <Outlet />

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