Skip to content

v1.10.0

Compare
Choose a tag to compare
@chaance chaance released this 09 Jan 22:16
· 127 commits to main since this release

The first release of 2023 is a big one for us. Remix 1.10 completes the React Router-ing of Remix and puts us in a solid position to ship some really exciting features in the new year. Let's dig in.

Rebuilt on React Router's data APIs

All of the data loading and mutation APIs you love in Remix are now completely built into the new framework-agnostic @remix-run/router package. This layer serves as the foundation of both React Router and Remix, and it provides a canvas upon which new integrations can be built moving forward. The community has already started building some really exciting experiments with this, and we're cooking up a few cool things ourselves 🌶️.

As a Remix user, nothing about your code or its behavior should change. But the new implementation opens several new possibilities we think you'll love, and it allows us to quickly start bringing new React Router features into Remix (like sending promises over the wire 🤯).

And if you have a React Router app you've been thinking about migrating to Remix, you can be confident that using the new APIs in v6.4 will work the same way when you're ready to make the move (really though, we think you should make the move).

If you have any questions on these new APIs, head on over to their official documentation in the React Router docs.

Higher level control of revalidation

Exporting a shouldRevalidate function from a route module gives you the ability to fine-tune how and when your route loaders are called.

Remix handles revalidation for you in many scenarios to keep your UI in sync with your data automatically. By default, route data is revalidated when:

  • After an action is called from a <Form>, <fetcher.Form>, useSubmit or fetcher.submit
  • When the URL search params change on the current route
  • When dynamic route params change on the current route
  • When the user navigates to the same URL

If shouldRevalidate is exported from a route module, it will call the function before calling the route loader for new data. If the function returns false, then the loader will not be called and the existing data for that loader will persist on the page. This is an optimization that can be used to avoid unnecessary database calls or other expensive operations.

// routes/invoices.jsx
export async function loader({ request }) {
  let url = new URL(request.url);
  let page = Number(url.searchParams.get("p") || 1);
  let limit = 20;
  return json(await getInvoices({ limit, offset: (page - 1) * limit }));
}

export function shouldRevalidate({ currentUrl }) {
  // Submissions shouldn't trigger a reload on most navigations
  // under `invoices`, so we only revalidate if the submission
  // originates from the nested `/invoices/new` route
  return currentUrl.pathname === "/invoices/new";
}

// routes/invoices.new.jsx
// The loader in `routes/invoices.jsx` will be revalidated after
// this action is called since the route's pathname is `/invoices/new`
export async function action({ request }) {
  let invoice = await createInvoice(await request.formData());
  return redirect(`/invoices/${invoice.id}`);
}

// routes/invoices/$invoiceId.jsx
// The loader in `routes/invoices.jsx` will *not* be revalidated after
// this action is called
export async function action({ request }) {
  let invoice = await updateInvoice(await request.formData());
  return json(invoice);
}

If you were already using unstable_shouldReload, note that it is now deprecated in favor of shouldRevalidate. Rename the export to shouldRevalidate and update the function to match the stable API:

export function shouldRevalidate({
  currentUrl,
  currentParams,
  nextUrl,
  nextParams,
  formMethod,
  formAction,
  formEncType,
  formData,
  actionResult,
  defaultShouldRevalidate,
}) {
  return true;
}

New hooks 🪝

useNavigation

When Remix was initially designed, React 18 and its concurrent features were still in the early stages of development, and its useTransition hook had not yet landed. Now that it has, we decided to rename our useTransition hook to useNavigation to avoid naming conflicts and confusion. useTransition will be deprecated in favor of useNavigation with a slightly updated API:

let transition = useTransition();
let navigation = useNavigation();

navigation.state; // same as transition.state
navigation.location; // same as transition.location

// data flatted from transition.submission
navigation.formData; // any form data submitted with the navigation
navigation.formMethod; // 'GET' or 'POST'
navigation.formAction; // The action URL to which the form data is submitted

The type property from useTransition was confusing for many users, so we have removed it in useNavigation. All of the information you need to inspect route transitions is available in useNavigation in, we think, a much simpler and easier-to-understand interface. All of the use cases for transition.type are possible with navigation.state and adding information to your action/loader/form data instead.

useNavigationType

We've also exposed the useNavigationType hook from React Router that gives you a bit more introspection into how the user is navigating.

// The user is navigating to a new URL.
useNavigationType() === "PUSH";

// The user is navigating back or forward in history.
useNavigationType() === "POP";

// The user is navigating but replacing the entry in
// history instead of pushing a new one.
useNavigationType() === "REPLACE";

useRevalidator

This hook allows you to revalidate data in your route for any reason. As noted above, Remix automatically revalidates the data after actions are called, but you may want to revalidate for other reasons. For example, you may want to revalidate data after a client-side interaction, in response to events from a websocket, or if the window is re-focused after the user re-activates the browser tab.

import { useRevalidator, useLoaderData } from "@remix-run/react";

function SomeRoute() {
  let loaderData = useLoaderData();
  let revalidator = useRevalidator();
  useWindowFocus(() => {
    revalidator.revalidate();
  });
  if (revalidator.state !== "idle") {
    return <div>Revalidating...</div>;
  }
  return <div>{loaderData.superFresh}</div>;
}

useRouteLoaderData

This hook makes the data at any currently rendered route available anywhere in the tree. This is useful for components deep in the tree that need data from routes higher up, or parent routes that need data from one of its child routes.

// routes/invoices.jsx
import { Outlet, useLoaderData, useRouteLoaderData } from "@remix-run/react";

export default function Invoices() {
  let allInvoices = useLoaderData();
  let currentInvoice = useRouteLoaderData("routes/invoices/$invoiceId");
  return (
    <div>
      <nav>
        <h2>Invoices</h2>
        {allInvoices.map((invoice) => (
          <div key={invoice.id}>
            <Link to={`/invoices/${invoice.id}`}>{invoice.name}</Link>
            {currentInvoice?.id === invoice.id && (
              <dl>
                <dt>Amount</dt>
                <dd>{invoice.amount}</dd>
                <dt>Due Date</dt>
                <dd>{invoice.dueDate}</dd>
              </dl>
            )}
          </div>
        ))}
        <main>
          <Outlet />
        </main>
      </nav>
    </div>
  );
}

Other Notes

Here's a few other small changes to be aware of:

  • fetcher.load calls now participate in revalidation - they always should have to avoid stale data on your page!
  • <ScrollRestoration> has a new getKey prop
  • <Link> has a new preventScrollReset prop

Changes by Package