From 5222956dfe9ba3714fbc67b8fa3b727d243cc108 Mon Sep 17 00:00:00 2001 From: Pedro Cattori Date: Sun, 21 Apr 2024 12:27:15 -0400 Subject: [PATCH 1/8] opt-in type inference for single-fetch --- .changeset/young-eagles-grab.md | 34 +++++++++++++ packages/remix-react/future/single-fetch.d.ts | 51 +++++++++++++++++++ packages/remix-react/rollup.config.js | 1 + 3 files changed, 86 insertions(+) create mode 100644 .changeset/young-eagles-grab.md create mode 100644 packages/remix-react/future/single-fetch.d.ts diff --git a/.changeset/young-eagles-grab.md b/.changeset/young-eagles-grab.md new file mode 100644 index 00000000000..1b18068d14a --- /dev/null +++ b/.changeset/young-eagles-grab.md @@ -0,0 +1,34 @@ +--- +"@remix-run/react": patch +--- + +Opt-in types for single-fetch + +To opt-in to type inference for single-fetch, add `future/single-fetch.d.ts` to `include` in your `tsconfig.json`: + +```json +{ + "include": [ + "./node_modules/@remix-run/react/future/single-fetch.d.ts" + ] +} +``` + +This changes `useLoaderData` and `useActionData` types to return single-fetch aware types instead of `SerializedFrom` types: + + +```ts +const loader = () => { + return { hello: "world", date: new Date() } +} + +// Without opting into single-fetch types +// Types from `loader` are serialized via `JSON.stringify` and `JSON.parse` +const before = useLoaderData(); +// ^? { hello: string, date: string } + +// Opting into single-fetch types +// Types from `loader` are serialized via `turbo-stream` +const after = useLoaderData(); +// ^? { hello: string, date: Date } +``` diff --git a/packages/remix-react/future/single-fetch.d.ts b/packages/remix-react/future/single-fetch.d.ts new file mode 100644 index 00000000000..65b3f4adca5 --- /dev/null +++ b/packages/remix-react/future/single-fetch.d.ts @@ -0,0 +1,51 @@ +import type { AppLoadContext } from "@remix-run/server-runtime"; + +type Serializable = + | undefined + | null + | boolean + | string + | symbol + | number + | Array + | { [key: PropertyKey]: Serializable } + | bigint + | Date + | URL + | RegExp + | Error + | Map + | Set + | Promise; + +type Params = { + readonly [key in Key]: string | undefined; +}; + +type ResponseStub = { + status?: number; + headers: Headers; +}; + +type DataFunction = ( + args: { + request: Request; + params: Params; + context: AppLoadContext; + response: ResponseStub; + }, + handlerCtx?: unknown +) => Serializable; + +type Loader = DataFunction & { hydrate?: boolean }; +type Action = DataFunction; + +declare module "@remix-run/react" { + export function useLoaderData(): T extends Loader + ? Awaited> + : never; + + export function useActionData(): T extends Action + ? Awaited> + : never; +} diff --git a/packages/remix-react/rollup.config.js b/packages/remix-react/rollup.config.js index 5ad9ecd1c06..20d9673a71f 100644 --- a/packages/remix-react/rollup.config.js +++ b/packages/remix-react/rollup.config.js @@ -51,6 +51,7 @@ module.exports = function rollup() { { src: "LICENSE.md", dest: [outputDir, sourceDir] }, { src: `${sourceDir}/package.json`, dest: outputDir }, { src: `${sourceDir}/README.md`, dest: outputDir }, + { src: `${sourceDir}/future`, dest: outputDir }, ], }), copyToPlaygrounds(), From 6c4c5895143f50f56ad36020f4c623369ce60f59 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Mon, 22 Apr 2024 10:54:19 -0400 Subject: [PATCH 2/8] Update single fetch type overrides to handle json/defer --- packages/remix-react/future/single-fetch.d.ts | 49 +++++++++++-------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/packages/remix-react/future/single-fetch.d.ts b/packages/remix-react/future/single-fetch.d.ts index 65b3f4adca5..b2bf9c74390 100644 --- a/packages/remix-react/future/single-fetch.d.ts +++ b/packages/remix-react/future/single-fetch.d.ts @@ -1,4 +1,10 @@ -import type { AppLoadContext } from "@remix-run/server-runtime"; +import type { + ActionFunctionArgs, + LoaderFunctionArgs, + SerializeFrom, + TypedDeferredData, + TypedResponse, +} from "@remix-run/server-runtime"; type Serializable = | undefined @@ -18,34 +24,37 @@ type Serializable = | Set | Promise; -type Params = { - readonly [key in Key]: string | undefined; -}; +type DataFunctionReturnValue = + | Serializable + | TypedDeferredData> + | TypedResponse>; -type ResponseStub = { - status?: number; - headers: Headers; +type Loader = (( + args: LoaderFunctionArgs +) => Promise) & { + hydrate?: boolean; }; -type DataFunction = ( - args: { - request: Request; - params: Params; - context: AppLoadContext; - response: ResponseStub; - }, - handlerCtx?: unknown -) => Serializable; +type Action = (args: ActionFunctionArgs) => Promise; -type Loader = DataFunction & { hydrate?: boolean }; -type Action = DataFunction; +// Backwards-compatible type for Remix v2 where json/defer still use the old types, +// and only non-json/defer returns use the new types. This allows for incremental +// migration of loaders to return naked objects. In the next major version, +// json/defer will be removed so everything will use the new simplified typings. +type SingleFetchSerialize_V2 = Awaited< + ReturnType +> extends TypedDeferredData> + ? SerializeFrom + : Awaited> extends TypedResponse> + ? SerializeFrom + : Awaited>; declare module "@remix-run/react" { export function useLoaderData(): T extends Loader - ? Awaited> + ? SingleFetchSerialize_V2 : never; export function useActionData(): T extends Action - ? Awaited> + ? SingleFetchSerialize_V2 : never; } From 053ed4c202a76ceed4687609757e17f9302c50aa Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Mon, 22 Apr 2024 11:01:42 -0400 Subject: [PATCH 3/8] Update docs --- docs/guides/single-fetch.md | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/docs/guides/single-fetch.md b/docs/guides/single-fetch.md index f63d928b744..ea823e5a3b4 100644 --- a/docs/guides/single-fetch.md +++ b/docs/guides/single-fetch.md @@ -51,11 +51,26 @@ You can control this by exporting a `streamTimeout` numeric value from your `ent ### Type Inference -The current generics support type inference but have a built-in assumption of a JSON-serialized response. With the new streaming format, this assumption no longer holds so `useLoaderData()` will _not_ return the proper types because it would assume that a `Date` would be a string on the client 😕. Unfortunately, we can't make these types aware of a runtime future flag and we do not want to introduce another hook just for this. Thankfully, the manual typing is much simpler without needing to think about JSON serialization, so the current recommendation is to skip the generics when opting into single fetch and manually cast the type yourself: +The current generics support type inference but have a built-in assumption of a JSON-serialized response. So, if you return a Javascript object without using the `json` utility, Remix will convert that to a JSON `Response` internally. + +With the new streaming format, this assumption no longer holds, so `useLoaderData()` in it's current form will _not_ return the proper types because it would assume that a `Date` would be a string on the client 😕. + +In order to ensure you get the proper types when using Single Fetch, we've included a set of type overrides that you can include in your `tsconfig.json` which aligns the `useLoaderData`/`useActionData`/etc. types with the Single Fetch behavior: + +```json +{ + "include": [ + // ... + "./node_modules/@remix-run/react/future/single-fetch.d.ts" + ] +} +``` + +This will update your typings to reflect the advanced serialization capabilities provided by Single Fetch's streaming format: ```ts export async function loader() { - const data = await fetchSomeData(); // Assume this returns + const data = await fetchSomeData(); return { message: data.message, // <- string date: data.date, // <- Date @@ -63,28 +78,16 @@ export async function loader() { } export default function Component() { - // ❌ Before + // ❌ Before opting into single fetch types, types are serialized via JSON.stringify const data = useLoaderData(); // ^? { message: string, date: string } - // ✅ After - const data = useLoaderData() as unknown as Awaited< - ReturnType - >; + // ✅ After opting into single fetch types, types are serialized via turbo-stream + const data = useLoaderData; // ^? { message: string, date: Date } } ``` -In the next version of Remix, we may re-introduce this generic, but in the meantime you could wrap this up into your own utility: - -```ts -function useTypedLoaderData() { - return useLoaderData() as unknown as Awaited< - ReturnType - >; -} -``` - ### Revalidations Previously, Remix would always revalidate all active loaders after _any_ action submission, regardless of the result of the action. You could opt-out of revalidation on a per-route basis via [`shouldRevalidate`][should-revalidate]. From 4bca4ddebbee4d79b853651b75516be22e68081d Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Mon, 22 Apr 2024 14:56:31 -0400 Subject: [PATCH 4/8] Add type handling for useFetcher, useMatches, meta --- packages/remix-react/future/single-fetch.d.ts | 105 ++++++++++++++---- packages/remix-react/index.tsx | 1 + 2 files changed, 84 insertions(+), 22 deletions(-) diff --git a/packages/remix-react/future/single-fetch.d.ts b/packages/remix-react/future/single-fetch.d.ts index b2bf9c74390..7103e4908c4 100644 --- a/packages/remix-react/future/single-fetch.d.ts +++ b/packages/remix-react/future/single-fetch.d.ts @@ -1,10 +1,15 @@ +import type { MetaArgs, UNSAFE_MetaMatch } from "@remix-run/react"; import type { - ActionFunctionArgs, - LoaderFunctionArgs, + LoaderFunction, + ActionFunction, SerializeFrom, TypedDeferredData, TypedResponse, } from "@remix-run/server-runtime"; +import type { + useFetcher as useFetcherRR, + FetcherWithComponents, +} from "react-router-dom"; type Serializable = | undefined @@ -24,37 +29,93 @@ type Serializable = | Set | Promise; -type DataFunctionReturnValue = - | Serializable - | TypedDeferredData> - | TypedResponse>; - -type Loader = (( - args: LoaderFunctionArgs -) => Promise) & { - hydrate?: boolean; -}; +// type DataFunctionReturnValue = +// | Serializable +// | TypedDeferredData> +// | TypedResponse>; -type Action = (args: ActionFunctionArgs) => Promise; +// type Loader = (args: LoaderFunctionArgs) => Promise; +// type Action = (args: ActionFunctionArgs) => Promise; // Backwards-compatible type for Remix v2 where json/defer still use the old types, // and only non-json/defer returns use the new types. This allows for incremental // migration of loaders to return naked objects. In the next major version, // json/defer will be removed so everything will use the new simplified typings. -type SingleFetchSerialize_V2 = Awaited< - ReturnType -> extends TypedDeferredData> - ? SerializeFrom - : Awaited> extends TypedResponse> - ? SerializeFrom - : Awaited>; +// prettier-ignore +type SingleFetchSerialize_V2 = + Awaited> extends TypedDeferredData ? D : + Awaited> extends TypedResponse> ? SerializeFrom : + Awaited>; + +// interface SingleFetchUIMatch_V2 +// extends UIMatchRR< +// D extends Loader | Action ? SingleFetchSerialize_V2 : never, +// H +// > {} declare module "@remix-run/react" { - export function useLoaderData(): T extends Loader + export function useLoaderData(): T extends LoaderFunction ? SingleFetchSerialize_V2 : never; - export function useActionData(): T extends Action + export function useActionData(): T extends ActionFunction ? SingleFetchSerialize_V2 : never; + + export function useRouteLoaderData( + routeId: string + ): T extends LoaderFunction ? SingleFetchSerialize_V2 : never; + + export function useFetcher( + opts?: Parameters[0] + ): FetcherWithComponents< + TData extends LoaderFunction | ActionFunction + ? SingleFetchSerialize_V2 + : never + >; + + export type UIMatch_SingleFetch = Omit< + UIMatch, + "data" + > & { + data: D extends LoaderFunction ? SingleFetchSerialize_V2 : never; + }; + + interface MetaMatch_SingleFetch< + RouteId extends string = string, + Loader extends LoaderFunction | unknown = unknown + > extends Omit, "data"> { + data: Loader extends LoaderFunction + ? SingleFetchSerialize_V2 + : unknown; + } + + type MetaMatches_SingleFetch< + MatchLoaders extends Record = Record< + string, + unknown + > + > = Array< + { + [K in keyof MatchLoaders]: MetaMatch_SingleFetch< + Exclude, + MatchLoaders[K] + >; + }[keyof MatchLoaders] + >; + + export interface MetaArgs_SingleFetch< + Loader extends LoaderFunction | unknown = unknown, + MatchLoaders extends Record = Record< + string, + unknown + > + > extends Omit, "data" | "matches"> { + data: + | (Loader extends LoaderFunction + ? SingleFetchSerialize_V2 + : unknown) + | undefined; + matches: MetaMatches_SingleFetch; + } } diff --git a/packages/remix-react/index.tsx b/packages/remix-react/index.tsx index b4336440722..ecb54165e15 100644 --- a/packages/remix-react/index.tsx +++ b/packages/remix-react/index.tsx @@ -101,6 +101,7 @@ export type { ClientLoaderFunction, ClientLoaderFunctionArgs, MetaArgs, + MetaMatch as UNSAFE_MetaMatch, MetaDescriptor, MetaFunction, RouteModules as UNSAFE_RouteModules, From 06c962307d5667ec3a3657d1db9e43c551b6fc9d Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Mon, 22 Apr 2024 15:07:28 -0400 Subject: [PATCH 5/8] Add back in return type narrowing --- docs/guides/single-fetch.md | 32 +++++++++- packages/remix-react/future/single-fetch.d.ts | 60 +++++++++---------- 2 files changed, 59 insertions(+), 33 deletions(-) diff --git a/docs/guides/single-fetch.md b/docs/guides/single-fetch.md index ea823e5a3b4..3d5c5d533d6 100644 --- a/docs/guides/single-fetch.md +++ b/docs/guides/single-fetch.md @@ -51,11 +51,11 @@ You can control this by exporting a `streamTimeout` numeric value from your `ent ### Type Inference -The current generics support type inference but have a built-in assumption of a JSON-serialized response. So, if you return a Javascript object without using the `json` utility, Remix will convert that to a JSON `Response` internally. +The current generics support type inference but have a built-in assumption of JSON-serialized responses. So, if you return a Javascript object from a `loader` or `action` without using the `json` utility, Remix will convert that to a JSON `Response` internally. -With the new streaming format, this assumption no longer holds, so `useLoaderData()` in it's current form will _not_ return the proper types because it would assume that a `Date` would be a string on the client 😕. +With the new streaming format, this assumption no longer holds, so `useLoaderData()` and other related generics (`useActionData`, `useRouteLoaderData`, `useFetcher`) in their current form will _not_ indicate the proper types because it would assume that a `Date` would be a string on the client 😕. -In order to ensure you get the proper types when using Single Fetch, we've included a set of type overrides that you can include in your `tsconfig.json` which aligns the `useLoaderData`/`useActionData`/etc. types with the Single Fetch behavior: +In order to ensure you get the proper types when using Single Fetch, we've included a set of type overrides that you can include in your `tsconfig.json` which aligns the types with the Single Fetch behavior: ```json { @@ -88,6 +88,32 @@ export default function Component() { } ``` +In some cases, we couldn't strictly override the generics verbatim, so we had to introduce a new type you can use when opting into Single Fetch: + +**useMatches** + +`useMatches`, requires a manual cast to specify the loader type in order to get proper type inference on a given `match.data`. When using Single Fetch, you will need to replace the `UIMatch` type with `UIMatch_SingleFetch`: + +```diff + let matches = useMatches(); +- let rootMatch = matches[0] as UIMatch; ++ let rootMatch = matches[0] as UIMatch_SingleFetch; +``` + +**`meta` Function** + +`meta` functions also require generic to indicate the current and ancestor route loader types in order to properly type the `data` and `matches` parameters. When using Single Fetch, you will need to replace the `MetaArgs` type with `MetaArgs_SingleFetch`: + +```diff + export function meta({ + data, + matches, +- }: MetaArgs) { ++ }: MetaArgs_SingleFetch) { + // ... + } +``` + ### Revalidations Previously, Remix would always revalidate all active loaders after _any_ action submission, regardless of the result of the action. You could opt-out of revalidation on a per-route basis via [`shouldRevalidate`][should-revalidate]. diff --git a/packages/remix-react/future/single-fetch.d.ts b/packages/remix-react/future/single-fetch.d.ts index 7103e4908c4..3ea4d76b710 100644 --- a/packages/remix-react/future/single-fetch.d.ts +++ b/packages/remix-react/future/single-fetch.d.ts @@ -1,7 +1,7 @@ import type { MetaArgs, UNSAFE_MetaMatch } from "@remix-run/react"; import type { - LoaderFunction, - ActionFunction, + LoaderFunctionArgs, + ActionFunctionArgs, SerializeFrom, TypedDeferredData, TypedResponse, @@ -29,47 +29,45 @@ type Serializable = | Set | Promise; -// type DataFunctionReturnValue = -// | Serializable -// | TypedDeferredData> -// | TypedResponse>; +type DataFunctionReturnValue = + | Serializable + | TypedDeferredData> + | TypedResponse>; -// type Loader = (args: LoaderFunctionArgs) => Promise; -// type Action = (args: ActionFunctionArgs) => Promise; +type LoaderFunction_SingleFetch = ( + args: LoaderFunctionArgs +) => Promise; +type ActionFunction_SingleFetch = ( + args: ActionFunctionArgs +) => Promise; // Backwards-compatible type for Remix v2 where json/defer still use the old types, // and only non-json/defer returns use the new types. This allows for incremental // migration of loaders to return naked objects. In the next major version, // json/defer will be removed so everything will use the new simplified typings. // prettier-ignore -type SingleFetchSerialize_V2 = +type SingleFetchSerialize_V2 = Awaited> extends TypedDeferredData ? D : Awaited> extends TypedResponse> ? SerializeFrom : Awaited>; -// interface SingleFetchUIMatch_V2 -// extends UIMatchRR< -// D extends Loader | Action ? SingleFetchSerialize_V2 : never, -// H -// > {} - declare module "@remix-run/react" { - export function useLoaderData(): T extends LoaderFunction + export function useLoaderData(): T extends LoaderFunction_SingleFetch ? SingleFetchSerialize_V2 : never; - export function useActionData(): T extends ActionFunction + export function useActionData(): T extends ActionFunction_SingleFetch ? SingleFetchSerialize_V2 : never; export function useRouteLoaderData( routeId: string - ): T extends LoaderFunction ? SingleFetchSerialize_V2 : never; + ): T extends LoaderFunction_SingleFetch ? SingleFetchSerialize_V2 : never; export function useFetcher( opts?: Parameters[0] ): FetcherWithComponents< - TData extends LoaderFunction | ActionFunction + TData extends LoaderFunction_SingleFetch | ActionFunction_SingleFetch ? SingleFetchSerialize_V2 : never >; @@ -78,23 +76,25 @@ declare module "@remix-run/react" { UIMatch, "data" > & { - data: D extends LoaderFunction ? SingleFetchSerialize_V2 : never; + data: D extends LoaderFunction_SingleFetch + ? SingleFetchSerialize_V2 + : never; }; interface MetaMatch_SingleFetch< RouteId extends string = string, - Loader extends LoaderFunction | unknown = unknown + Loader extends LoaderFunction_SingleFetch | unknown = unknown > extends Omit, "data"> { - data: Loader extends LoaderFunction + data: Loader extends LoaderFunction_SingleFetch ? SingleFetchSerialize_V2 : unknown; } type MetaMatches_SingleFetch< - MatchLoaders extends Record = Record< + MatchLoaders extends Record< string, - unknown - > + LoaderFunction_SingleFetch | unknown + > = Record > = Array< { [K in keyof MatchLoaders]: MetaMatch_SingleFetch< @@ -105,14 +105,14 @@ declare module "@remix-run/react" { >; export interface MetaArgs_SingleFetch< - Loader extends LoaderFunction | unknown = unknown, - MatchLoaders extends Record = Record< + Loader extends LoaderFunction_SingleFetch | unknown = unknown, + MatchLoaders extends Record< string, - unknown - > + LoaderFunction_SingleFetch | unknown + > = Record > extends Omit, "data" | "matches"> { data: - | (Loader extends LoaderFunction + | (Loader extends LoaderFunction_SingleFetch ? SingleFetchSerialize_V2 : unknown) | undefined; From 73d8199b011ab980440dd087bcbcfee95467fb88 Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Mon, 22 Apr 2024 15:23:20 -0400 Subject: [PATCH 6/8] Update docs --- docs/guides/single-fetch.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/guides/single-fetch.md b/docs/guides/single-fetch.md index 3d5c5d533d6..c8e98f31a22 100644 --- a/docs/guides/single-fetch.md +++ b/docs/guides/single-fetch.md @@ -51,9 +51,9 @@ You can control this by exporting a `streamTimeout` numeric value from your `ent ### Type Inference -The current generics support type inference but have a built-in assumption of JSON-serialized responses. So, if you return a Javascript object from a `loader` or `action` without using the `json` utility, Remix will convert that to a JSON `Response` internally. +the current type-inference in Remix has a built-in assumption of JSON-serialized responses. So, if you return a Javascript object from a `loader` or `action` without using the `json` utility, Remix will assume that object was converted to a JSON `Response` internally. -With the new streaming format, this assumption no longer holds, so `useLoaderData()` and other related generics (`useActionData`, `useRouteLoaderData`, `useFetcher`) in their current form will _not_ indicate the proper types because it would assume that a `Date` would be a string on the client 😕. +With the new streaming format, this assumption no longer holds, so some of the type built-in inference is no longer accurate once you have opted-into Single Fetch. For example, they would assume that a `Date` would be serialized to a string on the client 😕. In order to ensure you get the proper types when using Single Fetch, we've included a set of type overrides that you can include in your `tsconfig.json` which aligns the types with the Single Fetch behavior: @@ -66,7 +66,9 @@ In order to ensure you get the proper types when using Single Fetch, we've inclu } ``` -This will update your typings to reflect the advanced serialization capabilities provided by Single Fetch's streaming format: +**`useLoaderData`, `useActionData`, `useRouteLoaderData`, and `useFetcher`** + +These methods do not require any code changes on your part - adding the single fetch types will cause their generics to deserialize correctly: ```ts export async function loader() { @@ -88,11 +90,9 @@ export default function Component() { } ``` -In some cases, we couldn't strictly override the generics verbatim, so we had to introduce a new type you can use when opting into Single Fetch: - -**useMatches** +**`useMatches`** -`useMatches`, requires a manual cast to specify the loader type in order to get proper type inference on a given `match.data`. When using Single Fetch, you will need to replace the `UIMatch` type with `UIMatch_SingleFetch`: +`useMatches` requires a manual cast to specify the loader type in order to get proper type inference on a given `match.data`. When using Single Fetch, you will need to replace the `UIMatch` type with `UIMatch_SingleFetch`: ```diff let matches = useMatches(); @@ -102,7 +102,7 @@ In some cases, we couldn't strictly override the generics verbatim, so we had to **`meta` Function** -`meta` functions also require generic to indicate the current and ancestor route loader types in order to properly type the `data` and `matches` parameters. When using Single Fetch, you will need to replace the `MetaArgs` type with `MetaArgs_SingleFetch`: +`meta` functions also require a generic to indicate the current and ancestor route loader types in order to properly type the `data` and `matches` parameters. When using Single Fetch, you will need to replace the `MetaArgs` type with `MetaArgs_SingleFetch`: ```diff export function meta({ From ab40ad762e4cae413542adf21b60f327c1fe90bd Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Mon, 22 Apr 2024 15:24:35 -0400 Subject: [PATCH 7/8] Update changeset --- .changeset/young-eagles-grab.md | 30 +----------------------------- 1 file changed, 1 insertion(+), 29 deletions(-) diff --git a/.changeset/young-eagles-grab.md b/.changeset/young-eagles-grab.md index 1b18068d14a..784b5b7c859 100644 --- a/.changeset/young-eagles-grab.md +++ b/.changeset/young-eagles-grab.md @@ -3,32 +3,4 @@ --- Opt-in types for single-fetch - -To opt-in to type inference for single-fetch, add `future/single-fetch.d.ts` to `include` in your `tsconfig.json`: - -```json -{ - "include": [ - "./node_modules/@remix-run/react/future/single-fetch.d.ts" - ] -} -``` - -This changes `useLoaderData` and `useActionData` types to return single-fetch aware types instead of `SerializedFrom` types: - - -```ts -const loader = () => { - return { hello: "world", date: new Date() } -} - -// Without opting into single-fetch types -// Types from `loader` are serialized via `JSON.stringify` and `JSON.parse` -const before = useLoaderData(); -// ^? { hello: string, date: string } - -// Opting into single-fetch types -// Types from `loader` are serialized via `turbo-stream` -const after = useLoaderData(); -// ^? { hello: string, date: Date } -``` +- To opt-in to type inference for single-fetch, add `./node_modules/@remix-run/react/future/single-fetch.d.ts` to `include` in your `tsconfig.json` From f66de1a46294a1e363ad8ea9cadbc795043786ae Mon Sep 17 00:00:00 2001 From: Matt Brophy Date: Mon, 22 Apr 2024 15:43:14 -0400 Subject: [PATCH 8/8] Update docs --- docs/guides/single-fetch.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/guides/single-fetch.md b/docs/guides/single-fetch.md index c8e98f31a22..8f66f81e651 100644 --- a/docs/guides/single-fetch.md +++ b/docs/guides/single-fetch.md @@ -51,9 +51,9 @@ You can control this by exporting a `streamTimeout` numeric value from your `ent ### Type Inference -the current type-inference in Remix has a built-in assumption of JSON-serialized responses. So, if you return a Javascript object from a `loader` or `action` without using the `json` utility, Remix will assume that object was converted to a JSON `Response` internally. +Without Single Fetch, any plain Javascript object returned from a `loader` or `action` is automatically serialized into a JSON response (as if you returned it via `json`). The type inference assumes this is the case and infer naked object returns as if they were JSON serialized. -With the new streaming format, this assumption no longer holds, so some of the type built-in inference is no longer accurate once you have opted-into Single Fetch. For example, they would assume that a `Date` would be serialized to a string on the client 😕. +With Single Fetch, naked objects will be streamed directly, so the built-in type inference is no longer accurate once you have opted-into Single Fetch. For example, they would assume that a `Date` would be serialized to a string on the client 😕. In order to ensure you get the proper types when using Single Fetch, we've included a set of type overrides that you can include in your `tsconfig.json` which aligns the types with the Single Fetch behavior: