Skip to content

Releases: remix-run/remix

v1.11.1

21 Jan 00:33
80c2042
Compare
Choose a tag to compare

Fixed a bug with v2_routeConvention that prevented index files from being recognized as route modules

v1.11.0

19 Jan 01:22
1cda61c
Compare
Choose a tag to compare

New features.

That's it. That's 1.11.0 in a nutshell.

We're dropping a serious feature 💣 on you this week, so strap yourself in.

Promises over the wire with defer

Today we bring you one of our favorite features from React Router 6.4.

Remix aims to provide first-class support for React 18's SSR streaming capabilities. We can do that today with defer.

When you return a defer function call in a route loader, you are initiating a streamed response. This is very useful in cases where lower priority data may take a bit more time. You don't want your users to wait on the slow data if the important stuff is ready to roll.

import { json } from "@remix-run/node";

export async function loader({ request }) {
  let [productInfo, productReviews] = await Promise.all([
    // Product info query is small, cached aggressively,
    // and high priority for the user.
    getProductInfo(request),

    // Product reviews query is large and cache is more lax.
    // It also appears at the bottom of the page and is a lower
    // priority, so the user probably doesn't need it right away.
    getProductReviews(request),
  ]);

  // Without streaming, we gotta wait for both queries to resolve
  // before we can send a response. Our user is getting impatient.
  // They probably found something on your competitor's site that
  // loaded in the mean time. Wave goodbye to that new yacht you
  // were planning to buy with the earnings!
  return json({ productInfo, productReviews });
}

In these cases, the slower data is passed to defer as a promise, and everything else a resolved value.

import { defer } from "@remix-run/node";

export async function loader({ request }) {
  // Product info query is small, cached aggressively, and
  // high priority for the user. Let's go ahead and let it
  // resolve since it's fast!
  let productInfo = await getProductInfo(request);

  // Product reviews query is large and cache is more lax.
  // Let's initiate the query but not wait for it to resolve.
  let productReviewsPromise = getProductReviews(request);

  // With defer, we initate a streaming response. This allows
  // the user to access the resolved data (`productInfo`) as
  // soon as it's available, while the unresolved product
  // reviews are loaded in the background.
  // Enjoy the yacht, call us from Cabo!
  return defer({
    productInfo,
    productReviewsPromise,
  });
}

Now you may be thinking, this sounds cool … but what the heck do I do with a promise in my UI while my user waits for reviews? That's where <Await> comes in, with a little help from React Suspense.

import { Await } from "@remix-run/react";

function ProductRoute() {
  let {
    // Product info has already resolved. Render immediately!
    productInfo,
    // Product reviews might not be ready yet 🤔
    productReviewsPromise,
  } = useLoaderData();

  return (
    <div>
      <h1>{productInfo.name}</h1>
      <p>{productInfo.description}</p>
      <BuyNowButton productId={productInfo.id} />
      <hr />
      <h2>Reviews</h2>
      <React.Suspense fallback={<p>Loading reviews...</p>}>
        <Await resolve={productReviewsPromise} errorElement={<ReviewsError />}>
          {(productReviews) =>
            productReviews.map((review) => (
              <div key={review.id}>
                <h3>{review.title}</h3>
                <p>{review.body}</p>
              </div>
            ))
          }
        </Await>
      </React.Suspense>
    </div>
  );
}

// Error fetching the data? Slow connection timed out?
// Show an error message *only* for reviews. The rest
// of your product UI is still usable!
function ReviewsError() {
  let error = useAsyncError(); // Get the rejected value
  return <p>There was an error loading reviews: {error.message}</p>;
}

Documentation for the new feature can be found at the following links:

Built-in support for bundling CSS

Many common approaches to CSS within the React community are only possible when bundling CSS, meaning that the CSS files you write during development are collected into a separate bundle as part of the build process.

Remix has always left stylesheets up to you. All we cared about was having a static stylesheet to work with, but some CSS tools require deeper bundler integration to implement.

With this release, we can now support:

Unlike many other tools in the React ecosystem, we do not insert the CSS bundle into the page automatically. Instead, we ensure that you always have control over the link tags on your page. This lets you decide where the CSS file is loaded relative to other stylesheets in your app.

To get started, first install the new @remix-run/css-bundle package:

npm install @remix-run/css-bundle

Then, in your root route file, import cssBundleHref and include it in your links export, the same as you would any other stylesheet. This will load your bundled CSS in your entire app (though the same method could be used at any level in the route tree if you'd like!)

import { cssBundleHref } from "@remix-run/css-bundle";
import resetsStylesheetHref from "./resets.css";
import overridesStylesheetHref from "./overrides.css";

export function links() {
  return [
    { rel: "stylesheet", href: resetsStylesheetHref },
    { rel: "stylesheet", href: cssBundleHref },
    { rel: "stylesheet", href: overridesStylesheetHref },
  ];
}

Please note that these features are currently flagged as unstable. We're confident in the tools themselves, but the API and implementation may change in the future before a major release.

All three CSS bundling options are opt-in via the future key in remix.config.js:

module.exports = {
  future: {
    unstable_cssModules: true,
    unstable_vanillaExtract: true,
    unstable_cssSideEffectImports: true,
  },
};

For more details on each approach, check out our styling docs.

New route conventions for Remix v2

In the next major version of Remix, we will introduce a new default for how our routing conventions work. You can get a head start on upgrading your app by enabling the v2_routeConvention future flag in your Remix config.

The new convention allows for a flat directory structure for your routes. For apps with deeply nested routes, this can be a huge productivity boost as you no longer need to jump through several layers of folders to find what you're looking for.

But what if I like having a bunch of folders for my routes?

That's great! You can keep doing that today with no changes! 🥳

When we ship v2, you'll simply need to use the routes option in your Remix config to define the old route conventions. We'll be updating the docs and provide a helper function to make it easier for you to migrate without moving files around if that's what you prefer.

In the mean time, check out the RFC for the new routing convention to get a head start on things to come. We'll update the release notes as soon as the docs are polished ✨

v1.10.1

13 Jan 19:12
529322d
Compare
Choose a tag to compare

What's Changed

  • Fetchers should persist data through reload/resubmit (#5065)
  • Update babel config to transpile down to node 14 (#5047)

Full Changelog: 1.10.0...1.10.1

v1.10.0

09 Jan 22:16
Compare
Choose a tag to compare

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, or 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 stuff

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

  • fetcher.load calls now participate in revalidation, which should help to avoid stale data on your page
  • <ScrollRestoration> has a new getKey prop
  • <Link> has a new preventScrollReset prop

Changes by Package

v1.9.0

16 Dec 22:12
Compare
Choose a tag to compare

The New Stuff

Support for React Router's Optional Route Segments

We shipped the latest minor version of 6.5.0 with support for optional route segments, and now Remix supports them as well. To do this, we've introduced a new convention for file-system routes.

Route filenames surrounded by parenthesis will be converted into optional segments for React Router. For example /($lang)/about will be converted to /:lang?/about.

This means /($lang)/about would match:

/en/about
/fr/about
/about  <-- $lang is optional!

Another example: /(one)/($two)/(three).($four) route would match all of the following:

/
/one
/one/param1
/one/param1/three
/one/param1/three/param2

As with any of our conventions, you can escape the conversion by wrapping the route filename in square brackets. For example, /[(one)]/two would match the URL path /(one)/two.

Added Support for New TypeScript Syntax

The Remix compiler now supports new TypeScript 4.9 syntax (#4754). There were several cool features that landed in the latest TypeScript release, and we're stoked that you can grab them today! 🤓

One of our favorites is the satisfies keyword, which lets you validate that an expression matches a given type—without changing the resulting type of that expression.

// this example comes from the TypeScript 4.9 release notes
type Colors = "red" | "green" | "blue";
type RGB = [red: number, green: number, blue: number];
const palette = {
    red: [255, 0, 0],
    green: "#00ff00",
    bleu: [0, 0, 255]
//  ~~~~ The typo is now caught!
} satisfies Record<Colors, string | RGB>;
// Both of these methods are still accessible!
const redComponent = palette.red.at(0);
const greenNormalized = palette.green.toUpperCase();

For a closer look at all the new features available, check out the TypeScript release notes.

Perf Gains for Your Routes 💪

Sometimes you can get big wins out of tiny changes. We did that by making a tweak to a lookup algorithm in defineConventionalRoutes that resulted in some numbers we love to see.

In local runs of the production builds for a larger, realistic project (~700 routes):

  • Previously: 10-15s
  • Now: <1 second — >10x faster!

In addition to new features, we also squashed some nasty critters over the last week.

A Pesky Bug Squashed: Firefox and <LiveReload>

We fixed up a problem with <LiveReload> in Firefox that caused pages to infinitely reload after changes. This was no bueno!

The problem was:

  1. Firefox is calling ws.onclose immediately upon connecting (?!)
  2. Then we’re trying to reconnect, and upon reconnection, we reload the page
  3. Firefox then calls ws.onclose again after reconnecting and the loop starts over

This fix is to check for the proper event code (1006) before actually trying to reconnect and the reload the page. 1006 means the connection was closed abnormally, but in our case it means the server was shut down in local dev, and the socket can reconnect again when the server is back up.

Changes by Package

New Contributors


Full Changelog: v1.8.2...v1.9.0

v1.8.2

07 Dec 21:05
1a5b6f0
Compare
Choose a tag to compare

What's Changed

All the small things:

  • Remove instanceof Response checks in favor of isResponse (#4782)
  • Fix performance regression with creation of @remix-run/router static handler (#4790)
  • Update dependency for @remix-run/router to v1.0.5 (bd84a9317)

Full Changelog: v1.8.1...v1.8.2

v1.8.1

05 Dec 22:41
Compare
Choose a tag to compare

What's Changed

Patch Changes

  • Added a missing type definition for the Remix config future option to the @remix-run/dev/server-build virtual module that caused type-checking to fail in some runtime adapters (#4771)

Changes by package

New Contributors


Full Changelog: v1.8.0...v1.8.1

v1.8.0

01 Dec 14:47
Compare
Choose a tag to compare

What's Changed

Lots of goodies to dig in this week, so let's get right to it 💪

React-Routering Remix

We released React Router 6.4 a few weeks back. This update introduced many of Remix's data APIs and moved all of the core routing logic to a framework-agnostic @remix-run/router package. As a result, we needed to rebuild the Remix implementation to take advantage of these changes.

1.8.0 includes the first big step in that process, and Remix now performs all server-side data fetches and mutations through the new framework agnostic router. This is an implementation detail and doesn't change much for you today, but completing this work will open the door for Remix to support other frameworks in the future. 🤯

Our guy @brophdawg11 published React-Routering Remix to dive into the details, so we encourage you to check it out to get a better glimpse of things to come!

A more robust meta API

Each Remix route can export a meta function that will render <meta> elements in the head of the route's document. The existing meta API was designed to simplify some of the inconsistencies of various meta tag attributes, but in turn it made some meta tags more difficult—if not impossible—to use correctly.

We also automatically merged all meta tags in nested route trees. While this is desirable in many cases, it's not a decision we should make for you in the cases where a leaf route's meta should omit its parent's tags.

As a result, we are rolling out a new, lower-level API for meta. Now you will return an array of objects that map directly to the <meta> tag's attributes, exactly as we do for links. You also get access to the route matches with meta and data available so you can decide how you want to handle merging meta tags.

export function meta({ data, matches }) {
  // Want to snag some meta from a matched route? No problem!
  let rootModule = matches.find((match) => match.route.id === "root");

  // Only want to merge its og: tags? Easy breezy!
  let rootOgTags = rootModule.meta.filter((meta) =>
    meta.property?.startsWith("og:")
  );

  // Merge what you want, where you want. No more magic!
  return [
    ...rootOgTags,
    { title: "All Downhill from Here" },
    { property: "og:type", content: "music.song" },
    { property: "music:musician", content: "https://www.newfoundglory.com/" },
    { property: "music:duration", content: 192 },
    {
      property: "music:album",
      content: "https://open.spotify.com/album/1Igrcji3zf5aC61saylDE1",
    },
  ];
}

While this is a new API, the change is completely opt-in via the new future option in remix.config.js and it will become the default behavior in version 2.0. No breaking changes for you, but a clear and early upgrade path should you choose to take it! To opt in, add the following to your Remix config file:

module.exports = {
  future: {
    v2_meta: true,
  },
};

For more details on how we got here, check out the original RFC.

Making thrown Response objects less ambiguous

Previously there was some ambiguity around "thrown Responses go to the CatchBoundary". The CatchBoundary exists to give the user a place to handle non-happy path code flows such that they can throw Response instances from their own code and handle them in a CatchBoundary. However, there are a handful of framework-internal errors that make sense to have a non-500 status code, and the fact that these were being thrown as Response instances was causing them to go into the CatchBoundary, even though they were not thrown by the user.

With this change, anything thrown by the framework itself (Error or Response) will go to the ErrorBoundary, and any user-thrown Response instance will go to the CatchBoundary. There is one exception to this rule, which is that framework-detected 404 responses will continue to go to the CatchBoundary since users should have one single location to handle 404 displays.

The primary affected use cases are scenarios such as:

  • HTTP OPTIONS requests (405 Unsupported Method )
  • GET requests to routes without loaders (400 Bad Request)
  • POST requests to routes without actions (405 Method Not Allowed)
  • Missing route id in _data parameters (403 Forbidden)
  • Non-matching route id included in _data parameters (403 Forbidden)

For more information on this change, check out the PR in which it was introduced.

Other Changes

  • Add support for importing .wasm files (#3299)
  • Update @remix-run/web-fetch. This addresses two bugs: (#4644)
    • It fixes a memory leak caused by unregistered listeners
    • It adds support for custom "credentials" values (Remix does nothing with these at the moment, but they pass through for the consumer of the request to access if needed)
  • Ensure route modules are loaded even in failure cases
    • This addresses a long standing issue where you would end up in your root catch boundary if a form transition to another route threw. This no longer occurs, and you end up in the contextual boundary you'd expect. (#4611)

Changes by package

New Contributors


Full Changelog: v1.7.6...v1.8.0

v1.7.6

17 Nov 19:34
Compare
Choose a tag to compare

What Changed

Patch Changes

  • Fixed a regression in the browser build for browsers that don't support the nullish coalescing operator (#4561)

Changes by package

New Contributors


Full Changelog: v1.7.5...v1.7.6

v1.7.5

03 Nov 17:22
Compare
Choose a tag to compare

What Changed

Patch Changes

  • Make sure namespaced Open Graph and fb:app_id meta data renders the correct attributes on <meta> tags (#4445)
  • Add the missing event parameter type to useBeforeUnload (#1723)

Changes by package


Full Changelog: v1.7.4...v1.7.5