Releases: remix-run/remix
v1.11.1
Fixed a bug with v2_routeConvention
that prevented index
files from being recognized as route modules
v1.11.0
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:
- https://remix.run/docs/en/v1/guides/streaming
- https://remix.run/docs/en/v1/components/await
- https://remix.run/docs/en/v1/utils/defer
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:
- Direct CSS side-effect imports
- CSS Modules
- Vanilla Extract
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
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
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
orfetcher.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 newgetKey
prop<Link>
has a newpreventScrollReset
prop
Changes by Package
v1.9.0
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:
- Firefox is calling
ws.onclose
immediately upon connecting (?!) - Then we’re trying to reconnect, and upon reconnection, we reload the page
- 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
- @lucasferreira made their first contribution in #3970
- @dabdine made their first contribution in #4734
- @dmarkow made their first contribution in #4538
- @lordofthecactus made their first contribution in #4706
- @akamfoad made their first contribution in #4825
- @jsbmg made their first contribution in #4833
Full Changelog: v1.8.2...v1.9.0
v1.8.2
What's Changed
All the small things:
- Remove
instanceof Response
checks in favor ofisResponse
(#4782) - Fix performance regression with creation of
@remix-run/router
static handler (#4790) - Update dependency for
@remix-run/router
tov1.0.5
(bd84a9317
)
Full Changelog: v1.8.1...v1.8.2
v1.8.1
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
- @roj1512 made their first contribution in #4591
- @eltociear made their first contribution in #4477
- @AustinGil made their first contribution in #4283
- @mjangir made their first contribution in #4625
- @chaukhoa97 made their first contribution in #4688
- @shubhaguha made their first contribution in #4745
- @pmbanugo made their first contribution in #4767
Full Changelog: v1.8.0...v1.8.1
v1.8.0
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
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
- @wangbinyq made their first contribution in #4287
- @aissa-bouguern made their first contribution in #4465
- @lili21 made their first contribution in #4017
- @luisrivas made their first contribution in #4490
Full Changelog: v1.7.5...v1.7.6
v1.7.5
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