v0.21.0
Branches
main (2.15.0)dev
Versions
2.15.01.19.3v0.21.0
Viewing docs for an older release. View latest
On this page

v0.8.0 Release Notes

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;
Docs and examples licensed under MIT