Skip to content

Releases: remix-run/remix

v1.14.2

14 Mar 17:50
a92d877
Compare
Choose a tag to compare

1.14.2 doesn't contain any functional changes. It just removes a few deprecation warnings that inadvertently shipped in 1.14.1 but were intended for the upcoming 1.15.0 release.


Full Changelog: 1.14.1...1.14.2

v1.14.1

09 Mar 23:47
3ffb1a0
Compare
Choose a tag to compare

Just a bit of housekeeping and a few bug-fixes in this release! Check out the release notes for each package to see what we're up to 👀

New Contributors


Full Changelog: 1.14.0...1.14.1

v1.14.0

01 Mar 22:06
73fc576
Compare
Choose a tag to compare

New Development Server with HMR 🔥

You asked for it, and now we're stoked to deliver. We've got a brand new dev server that we think will dramatically improve the experience of running your Remix apps in development.

The new dev environment includes long-anticipated Hot Module Replacement (HMR) via React Refresh, as well as something we're calling Hot Data Revalidation (HDR)

HMR allows you to make changes to your UI or style code and see them reflected in your browser without having to refresh the page. This is different from our existing <LiveReload> component, as HMR will not reset client-side state between updates. This is particularly useful for highly-interactive apps where resetting state is disruptive and slows down the development process.

Now for HDR. Think of it as HMR for data loaders. With HDR you can make changes to your server code and see those updates reflected in your UI immediately without resetting client-side state, just like HMR.

This is an early release available under the unstable_dev future flag, but we're excited to get it into your hands, gather feedback and provide a first-class developer experience for apps at any scale. As of now, there are some known limitations to be aware of:

  • We don't yet expose an API for import.meta.hot
  • All route loaders are invalidated when changes are detected on the server
  • Loader changes do not account for changes in imported dependencies
  • It's doesn't work automatically with the Remix App Server, you'll want to bring in @remix-run/express for your server. This will not be a limitation when the unstable flag is removed.

Using HMR/HDR

First, you need to enable the new dev server in your remix.config.js:

module.exports = {
  // ...
  future: {
    unstable_dev: true,
  },
};

The new dev server and HMR/HDR requires two processes, one for your build and one for your app. You can run these in separate tabs or you can use something like npm-run-all to run them in parallel via a package.json script. We are also using nodemon to auto-restart our server on build changes. It's important to note that we're setting NODE_ENV=development here which is required to enable HMR/HDR.

Using the Remix App Server:

// package.json scripts
"dev": "run-p dev:*",
"dev:build": "cross-env NODE_ENV=development remix dev",
"dev:serve": "cross-env NODE_ENV=development nodemon --watch build node_modules/.bin/remix-serve build",

Using an Express Server:

// package.json scripts
"dev": "run-p dev:*",
"dev:build": "cross-env NODE_ENV=development remix dev",
"dev:serve": "cross-env NODE_ENV=development nodemon --watch build server.js",

Other notable changes

  • entry.server and entry.client files are now optional. If excluded, Remix will use reasonable defaults at build-time. If you need customization for these files, you can run npx remix reveal and it will generate them for you.
  • For users using the v2_routeConvention flag, route conflicts will no longer throw errors. Instead, you'll see a helpful warning that points to the conflict, and the first match we find will be used.
    ⚠️ Route Path Collision: "/dashboard"
    The following routes all define the same URL, only the first one will be used
    🟢️️ routes/dashboard/route.tsx
    ⭕️️ routes/dashboard.tsx
    
    ⚠️ Route Path Collision: "/"
    The following routes all define the same URL, only the first one will be used
    🟢️️ routes/_landing._index.tsx
    ⭕️️ routes/_dashboard._index.tsx
    ⭕️ routes/_index.tsx
    

Miscellaneous

v1.13.0

15 Feb 00:17
6967e5e
Compare
Choose a tag to compare

This Valentine's Day, get that special someone in your life what they really want: more styling options—out of the box—in Remix 🥰

Built-in PostCSS support

Remix can now process your existing CSS imports with PostCSS. A lot of folks have been doing this for quite some time, but in Remix this previously required you to run any CSS transformations as a separate process, and imports would need to reference the output rather than the source.

No longer! Now you can import references to the CSS files you actually write and Remix will take care of the rest. This is opt-in under the future.unstable_postcss flag in remix.config. From there, all you need is PostCSS configured in your app.

// remix.config.js
module.exports = {
  future: {
    unstable_postcss: true,
  },
};
// postcss.config.js
module.exports = {
  plugins: [/* your plugins here! */],
  presets: [/* your presets here! */],
};
// app/routes/root.jsx
// huzzah, the stylez are transformed before your very eyes!
import stylesheet from "./root.css";

export const links: LinksFunction = () => [
  { rel: "stylesheet", href: stylesheet },
];

Built-in Tailwind support

Valentine's Day is all about expressing how you feel, and what else evokes stronger feelings than Tailwind? 🙈

For all of our fellow lovers, you can now get your Tailwind styles generated without running a separate process. As with PostCSS, you'll need to opt-in (for now) with the future.unstable_tailwind flag.

// remix.config.js
module.exports = {
  future: {
    unstable_tailwind: true,
  },
};

If you haven't already, install Tailwind and initialize a config file.

npm install -D tailwindcss
npx tailwindcss init

Then you can create a stylesheet and use Tailwind directives wherever you'd like,

/* app/tailwind.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

Now you can import your stylesheet and link to it in your route, and you're good to go!

// app/routes/root.jsx
// huzzah, the stylez are transformed before your very eyes!
import stylesheet from "~/tailwind.css";

export const links: LinksFunction = () => [
  { rel: "stylesheet", href: stylesheet },
];

Fine-tuning your server build

We are deprecating serverBuildTarget in remix.config. Instead, you can target your server build with our more granular config options, granting you much more flexibility in where and how you ship.

Moving forward, you have a number of options that will help you configure your server bundle to fit your needs:

Fixes and enhancements for v2_routeConventions

We recently rolled out early experimental support for new route conventions to prepare you for Remix v2. We've fixed a few bugs and made a number of improvements for our eager early adopters.

One such enhancement we wanted to call out is how we can disambiguate "index routes" from "index modules". Turns out many people don't know about "index modules" or "folder imports" in node.js that our "flat route folders" depended on. Instead of trying to explain the difference, we went with a new convention: route.tsx.

When you want to split your route module out into multiple co-located modules, you can turn it into a folder and put the route at folder/route.tsx (or folder/route.jsx) and the rest of the modules in the folder will be ignored.

Consider this route:

routes/contact.tsx

When you want to split it up, you can make it a folder with a route.tsx file inside:

routes/contact/route.tsx <- this is the route module
routes/contact/form.tsx <- this is just a module, ignored by the route convention

For those of you who do know the difference between an index route and a node index module, index.tsx will continue to work inside folders, but we'd rather you use route.tsx

For more information, check out the docs for the v2 file convention.

Please note that this only applies if you have opted in to the new route conventions in future.v2_routeConventions. Current v1 behavior for file-system routing has not changed.

v1.12.0

31 Jan 00:10
f215e47
Compare
Choose a tag to compare

We've got a few nice bug fixes and improvements in this release. You can catch those in the individual package changelogs below, but what we really want to talk about today is a huge step forward in a much nicer developer experience for you. Let's get to it!

An early peek at a new dev server 👀

Since the early days of Remix, the remix dev command has been limited by its association with the built-in Remix app server while remix watch simply watched your files and spit out builds, but doesn't handle requests like some other tools. But one of the major benefits of this is that Remix lets you own your server. While this is a very powerful core feature of our architecture, it presented a few challenges for local development. If you own your server, how can we build a dev server that is flexible enough to work for your specific setup?

Our first answer was … we don't. Don't try to be clever, don't over-abstract anything. Simply run the remix watch, spit out files, and have <LiveReload> tell the browser to reload. But we didn't know if your app server was done rebooting the server code! And in the case of remix dev with remix-serve, we didn't reboot the server, we simply wiped out the node require cache as a budget server side HMR.

There were a few downsides for local development with this:

  1. Clearing the require cache wipes in-memory references, such as database connections or in-memory caches. This means you have to use workaround like sticking flags on global to persist anything stored in memory across rebuilds.
  2. Getting the web socket port to remix watch and over to your app server in a different process (whether it was express, netlify def, vercel dev, etc.) and using it in LiveReload was a lot of unnecessary friction.
  3. It makes implementing popular new dev features like hot-module replacement (HMR) much more difficult to implement in a way that works consistently across all app servers.
  4. Each rebuild writes new files to build/ and public/build/. As these files are not removed (unless the dev server crashes or is gracefully terminated), thousands of files could accumulate as the dev server ran. This causes performance issues and could be confusing when debugging.

To start addressing these issues, we decided to take a new approach. This release gives you early access to our new dev server and start making incremental improvements to your local development flow.

Please note that this is an unstable feature and a work-in-progress. But we think there are good reasons to opt in right away!

At this stage, the idea is to spin up the dev server alongside your normal Remix app server:

# spin up the new dev server
remix dev

# Spin up your app server in parallel.
# This can be done in a separate terminal or with a tool like `concurrently`.
nodemon --watch build/ ./server.js

Our dev server will build your app in development mode and then rebuild whenever any app files changes. It will also wait for your app server to signal that it's "ready" before triggering a reload in your browser.

No more wiping in-memory caches. No more weird hacks to keep your database alive. A fresh, clean slate for each rebuild.

While this may seem like a relatively small change, but it's removes a huge barrier for future DX improvements for local development. We think you're going to love what we're working on next 🔥🔁

Limitations

The new dev server does not currently work with Remix App Server (i.e. remix-serve command) because Remix App Server hardcodes NODE_ENV=production.

Rest assured, when the new dev server stabilizes it will support Remix App Server as well.

Configuring the unstable_dev server

To enable the new dev server with all defaults, set the unstable_dev future flag to true:

// remix.config.js
module.exports = {
  future: {
    unstable_dev: true,
  },
};

You can also set specific options. As this is an unstable feature, these options may change before the next major release.

// remix.config.js
module.exports = {
  future: {
    unstable_dev: {
      // Port to use for the dev server (i.e. the <LiveReload> websocket)
      // This can be overridden by a CLI flag: `remix dev --port 3011`
      // By default, we will find an empty port and use that
      port: 3010,

      // Port for running your Remix app server
      // This can be overridden by a CLI flag: `remix dev --app-server-port 3021`
      // default: `3000`
      appServerPort: 3020,

      // Path to the Remix request handler in your app server
      // Most app servers will route all requests to the Remix request
      // handler and will not need this option. If your app server _does_
      // route only certain request paths to the Remix request handler, then
      // you'll need to set this.
      // default: `""`
      remixRequestHandlerPath: "/products",

      // The number of milliseconds between "readiness" pings to your app server
      // When a Remix rebuild finishes, the dev server will ping a special
      // endpoint (`__REMIX_ASSETS_MANIFEST`) to check if your app server is
      // serving up-to-date routes and assets. You can set this option to tune
      // how frequently the dev server polls your app server.
      // default: `50`
      rebuildPollIntervalMs: 25,
    },
  },
};

Other Stuff

  • Updated to latest react-router versions. See the release notes for more details.
    • react-router-dom@6.8.0
    • @remix-run/router@1.3.1
  • You can now configure the client-side socket timeout via the new timeoutMs prop on <LiveReload /> (#4036)
  • <Link to> can now accept absolute URLs. When the to value is an absolute URL, the underlying anchor element will behave as normal, and its URL will not be prefetched. (#5092)
  • Added support for unstable_useBlocker and unstable_usePrompt from React Router (#5151)
  • Removed react & react-dom from peerDependencies (#4801)

Changes by Package 🔗

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