v0.9.0 Release Notes

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.


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!


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 (
      <h1>Meet the Team</h1>
        {team.map(member => (

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