Skip to content

v1.11.0

Compare
Choose a tag to compare
@chaance chaance released this 19 Jan 01:22
· 2204 commits to main since this release
1cda61c

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 ✨