diff --git a/CHANGELOG.md b/CHANGELOG.md index 27464a9686a..fd37336f2c8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -167,6 +167,76 @@ Date: YYYY-MM-DD --> +## v2.9.2 + +Date: 2024-05-10 + +### What's Changed + +#### Updated Type-Safety for Single Fetch + +In 2.9.2 we've enhanced the type-safety when opting into the `future.unstable_singleFetch` feature. Previously, we added the `response` stub to `LoaderFunctionArgs` and used type overrides for inference on `useLoaderData`, etc., but we found that it wasn't quite enough. + +With this release we're introducing new functions to assist the type-inference when using single fetch - `defineLoader`/`defineAction` and their client-side counterparts `defineClientLoader` and nd `defineClientAction`. These are identity functions; they don't modify your loader or action at runtime. Rather, they exist solely for type-safety by providing types for args and by ensuring valid return types. + +```ts +export let loader = defineLoader(({ request }) => { + // ^? Request + return { a: 1, b: () => 2 }; + // ^ type error: `b` is not serializable +}); +``` + +Note that `defineLoader` and `defineAction` are not technically necessary for defining loaders and actions if you aren't concerned with type-safety: + +```ts +// this totally works! and typechecking is happy too! +export let loader = () => { + return { a: 1 }; +}; +``` + +This means that you can opt-in to `defineLoader` incrementally, one loader at a time. + +Please see the [Single Fetch docs](https://remix.run/docs/en/main/guides/single-fetch) for more information. + +### Patch Changes + +- `@remix-run/dev` - Vite: Fix `dest already exists` error when running `remix vite:build` ([#9305](https://github.com/remix-run/remix/pull/9305)) +- `@remix-run/dev` - Vite: Fix issue resolving critical CSS during development when route files are located outside of the app directory ([#9194](https://github.com/remix-run/remix/pull/9194)) +- `@remix-run/dev` - Vite: Remove `@remix-run/node` from Vite plugin's `optimizeDeps.include` list since it was unnecessary and resulted in Vite warnings when not depending on this package ([#9287](https://github.com/remix-run/remix/pull/9287)) +- `@remix-run/dev` - Vite: Clean up redundant `?client-route=1` imports in development ([#9395](https://github.com/remix-run/remix/pull/9395)) +- `@remix-run/dev` - Vite: Ensure Babel config files are not referenced when applying the `react-refresh` Babel transform within the Remix Vite plugin ([#9241](https://github.com/remix-run/remix/pull/9241)) +- `@remix-run/react` - Type-safety for single-fetch: `defineLoader`, `defineClientLoader`, `defineAction`, `defineClientAction` ([#9372](https://github.com/remix-run/remix/pull/9372)) +- `@remix-run/react` - Single Fetch: Add `undefined` to `useActionData` type override ([#9322](https://github.com/remix-run/remix/pull/9322)) +- `@remix-run/react` - Single Fetch: Allow a `nonce` to be set on single fetch stream transfer inline scripts via `` ([#9364](https://github.com/remix-run/remix/pull/9364)) +- `@remix-run/server-runtime` - Single Fetch: Don't log thrown response stubs via `handleError` ([#9369](https://github.com/remix-run/remix/pull/9369)) +- `@remix-run/server-runtime` - Single Fetch: Automatically wrap resource route naked object returns in `json()` for back-compat in v2 (and log deprecation warning) ([#9349](https://github.com/remix-run/remix/pull/9349)) +- `@remix-run/server-runtime` - Single Fetch: Pass `response` stub to resource route handlers ([#9349](https://github.com/remix-run/remix/pull/9349)) + +### Updated Dependencies + +- [`react-router-dom@6.23.1`](https://github.com/remix-run/react-router/releases/tag/react-router%406.23.1) +- [`@remix-run/router@1.16.1`](https://github.com/remix-run/react-router/blob/main/packages/router/CHANGELOG.md#1161) + +### Changes by Package + +- [`@remix-run/cloudflare`](https://github.com/remix-run/remix/blob/remix%402.9.2/packages/remix-cloudflare/CHANGELOG.md#292) +- [`@remix-run/cloudflare-pages`](https://github.com/remix-run/remix/blob/remix%402.9.2/packages/remix-cloudflare-pages/CHANGELOG.md#292) +- [`@remix-run/cloudflare-workers`](https://github.com/remix-run/remix/blob/remix%402.9.2/packages/remix-cloudflare-workers/CHANGELOG.md#292) +- [`@remix-run/css-bundle`](https://github.com/remix-run/remix/blob/remix%402.9.2/packages/remix-css-bundle/CHANGELOG.md#292) +- [`@remix-run/deno`](https://github.com/remix-run/remix/blob/remix%402.9.2/packages/remix-deno/CHANGELOG.md#292) +- [`@remix-run/dev`](https://github.com/remix-run/remix/blob/remix%402.9.2/packages/remix-dev/CHANGELOG.md#292) +- [`@remix-run/eslint-config`](https://github.com/remix-run/remix/blob/remix%402.9.2/packages/remix-eslint-config/CHANGELOG.md#292) +- [`@remix-run/express`](https://github.com/remix-run/remix/blob/remix%402.9.2/packages/remix-express/CHANGELOG.md#292) +- [`@remix-run/node`](https://github.com/remix-run/remix/blob/remix%402.9.2/packages/remix-node/CHANGELOG.md#292) +- [`@remix-run/react`](https://github.com/remix-run/remix/blob/remix%402.9.2/packages/remix-react/CHANGELOG.md#292) +- [`@remix-run/serve`](https://github.com/remix-run/remix/blob/remix%402.9.2/packages/remix-serve/CHANGELOG.md#292) +- [`@remix-run/server-runtime`](https://github.com/remix-run/remix/blob/remix%402.9.2/packages/remix-server-runtime/CHANGELOG.md#292) +- [`@remix-run/testing`](https://github.com/remix-run/remix/blob/remix%402.9.2/packages/remix-testing/CHANGELOG.md#292) + +**Full Changelog**: [`v2.9.1...v2.9.2`](https://github.com/remix-run/remix/compare/remix@2.9.1...remix@2.9.2) + ## v2.9.1 Date: 2024-04-24 diff --git a/docs/guides/single-fetch.md b/docs/guides/single-fetch.md index 94df7249ba2..3b0410d4706 100644 --- a/docs/guides/single-fetch.md +++ b/docs/guides/single-fetch.md @@ -29,7 +29,7 @@ Previously, Remix used `JSON.stringify` to serialize your loader/action data ove With Single Fetch, Remix now uses [`turbo-stream`][turbo-stream] under the hood which provides first class support for streaming and allows you to automatically serialize/deserialize more complex data than JSON. The following data types can be streamed down directly via `turbo-stream`: `BigInt`, `Date`, `Error`, `Map`, `Promise`, `RegExp`, `Set`, `Symbol`, and `URL`. Subtypes of `Error` are also supported as long as they have a globally available constructor on the client (`SyntaxError`, `TypeError`, etc.). -This may or may not require any changes to your code once enabling Single Fetch: +This may or may not require any immediate changes to your code once enabling Single Fetch: - ✅ `json` responses returned from `loader`/`action` functions will still be serialized via `JSON.stringify` so if you return a `Date`, you'll receive a `string` from `useLoaderData`/`useActionData` - ⚠️ If you're returning a `defer` instance or a naked object, it will now be serialized via `turbo-stream`, so if you return a `Date`, you'll receive a `Date` from `useLoaderData`/`useActionData` @@ -37,6 +37,8 @@ This may or may not require any changes to your code once enabling Single Fetch: This also means that you no longer need to use the `defer` utility to send `Promise` instances over the wire! You can include a `Promise` anywhere in a naked object and pick it up on `useLoaderData().whatever`. You can also nest `Promise`'s if needed - but beware of potential UX implications. +Once adopting Single Fetch, it is recommended that you incrementally remove the usage of `json`/`defer` throughout your application in favor of returning raw objects. + ### React Rendering APIs In order to maintain consistency between document and data requests, `turbo-stream` is also used as the format for sending down data in initial document requests. This means that once opted-into Single Fetch, your application can no longer use [`renderToString`][rendertostring] and must use a React streaming renderer API such as [`renderToPipeableStream`][rendertopipeablestream] or [`renderToReadableStream`][rendertoreadablestream]) in [`entry.server.tsx`][entry-server]. @@ -113,7 +115,7 @@ In order to ensure you get the proper types when using Single Fetch, we've inclu 🚨 Make sure the single-fetch types come after any other Remix packages in `types` so that they override those existing types. -#### loader/action definition utilities +#### Loader/Action Definition Utilities To enhance type-safety when defining loaders and actions with single fetch, you can use the new `unstable_defineLoader` and `unstable_defineAction` utilities: @@ -161,6 +163,33 @@ type Serializable = | Promise; ``` +There are also client-side equivalents un `defineClientLoader`/`defineClientAction` that don't have the same return value restrictions because data returned from `clientLoader`/`clientAction` does not need to be serialized over the wire: + +```ts +import { unstable_defineLoader as defineLoader } from "@remix-run/node"; +import { unstable_defineClientLoader as defineClientLoader } from "@remix-run/react"; + +export const loader = defineLoader(() => { + return { msg: "Hello!", date: new Date() }; +}); + +export const clientLoader = defineClientLoader( + async ({ serverLoader }) => { + const data = await serverLoader(); + // ^? { msg: string, date: Date } + return { + ...data, + client: "World!", + }; + } +); + +export default function Component() { + const data = useLoaderData(); + // ^? { msg: string, date: Date, client: string } +} +``` + These utilities are primarily for type inference on `useLoaderData` and it's equivalents. If you have a resource route that returns a `Response` and is not consumed by Remix APIs (such as `useFetcher`) than you can just stick with your normal `loader`/`action` definitions. Converting those routes to use `defineLoader`/`defineAction` would cause type errors because `turbo-stream` cannot serialize a `Response` instance. #### `useLoaderData`, `useActionData`, `useRouteLoaderData`, `useFetcher` @@ -234,39 +263,32 @@ Instead, your `loader`/`action` functions now receive a mutable `ResponseStub` u - `response.headers.delete(name)` ```ts -type ResponseStub = { - status: number | undefined; - headers: Headers; -}; - -export async function action({ - request, - response, -}: { - request: Request; - response?: ResponseStub; -}) { - if (!loggedIn(request)) { - response.status = 401; - response.headers.append("Set-Cookie", "foo=bar"); - return { message: "Invalid Submission! " }; +export const action = defineAction( + async ({ request, response }) => { + if (!loggedIn(request)) { + response.status = 401; + response.headers.append("Set-Cookie", "foo=bar"); + return { message: "Invalid Submission! " }; + } + await addItemToDb(request); + return null; } - await addItemToDb(request); - return null; -} +); ``` You can also throw these response stubs to short circuit the flow of your loaders and actions: ```tsx -export async function loader({ request, response }) { - if (shouldRedirectToHome(request)) { - response.status = 302; - response.headers.set("Location", "/"); - throw response; +export const loader = defineLoader( + ({ request, response }) => { + if (shouldRedirectToHome(request)) { + response.status = 302; + response.headers.set("Location", "/"); + throw response; + } + // ... } - // ... -} +); ``` Each `loader`/`action` receives it's own unique `response` instance so you cannot see what other `loader`/`action` functions have set (which would be subject to race conditions). The resulting HTTP Response status and headers are determined as follows: @@ -285,7 +307,7 @@ Because single fetch supports naked object returns, and you no longer need to re ### Client Loaders -If your app has route using [`clientLoader`][client-loader] functions, it's important to note that the behavior of Single Fetch will change slightly. Because `clientLoader` is intended to give you a way to opt-out of calling the server `loader` function - it would be incorrect for the Single Fetch call to execute that server loader. But we run all loaders in parallel and we don't want to _wait_ to make the call until we know which `clientLoader`'s are actually asking for server data. +If your app has routes using [`clientLoader`][client-loader] functions, it's important to note that the behavior of Single Fetch will change slightly. Because `clientLoader` is intended to give you a way to opt-out of calling the server `loader` function - it would be incorrect for the Single Fetch call to execute that server loader. But we run all loaders in parallel and we don't want to _wait_ to make the call until we know which `clientLoader`'s are actually asking for server data. For example, consider the following `/a/b/c` routes: