Skip to content

Releases: remix-run/remix

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;