-
Notifications
You must be signed in to change notification settings - Fork 2.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Single-fetch typesafety: defineLoader
and defineAction
#9372
Changes from all commits
a977be6
ab4a44d
54693f3
4e0d411
cdbea62
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
--- | ||
"@remix-run/cloudflare": patch | ||
"@remix-run/deno": patch | ||
"@remix-run/node": patch | ||
"@remix-run/react": patch | ||
"@remix-run/server-runtime": patch | ||
--- | ||
|
||
Typesafety for single-fetch: defineLoader, defineClientLoader, defineAction, defineClientAction | ||
|
||
`defineLoader` and `defineAction` are helpers for authoring `loader`s and `action`s. | ||
They are identity functions; they don't modify your loader or action at runtime. | ||
Rather, they exist solely for typesafety 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 typesafety: | ||
|
||
```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. | ||
|
||
You can return custom responses via the `json`/`defer` utilities, but doing so will revert back to the old JSON-based typesafety mechanism: | ||
|
||
```ts | ||
let loader1 = () => { | ||
return {a: 1, b: new Date()} | ||
} | ||
let data1 = useLoaderData<typeof loader1>() | ||
// ^? {a: number, b: Date} | ||
|
||
let loader2 = () => { | ||
return json({a: 1, b: new Date()}) // this opts-out of turbo-stream | ||
} | ||
let data2 = useLoaderData<typeof loader2>() | ||
// ^? JsonifyObject<{a: number, b: Date}> which is really {a: number, b: string} | ||
``` | ||
|
||
You can also continue to return totally custom responses with `Response` though this continues to be outside of the typesystem since the built-in `Response` type is not generic |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -98,13 +98,66 @@ In order to ensure you get the proper types when using Single Fetch, we've inclu | |
|
||
```json | ||
{ | ||
"include": [ | ||
// ... | ||
"node_modules/@remix-run/react/future/single-fetch.d.ts" | ||
] | ||
"compilerOptions": { | ||
"types": ["@remix-run/react/future/single-fetch.d.ts"] | ||
} | ||
} | ||
``` | ||
|
||
🚨 Make sure the single-fetch types come after any other Remix packages in `types` so that they override those existing types. | ||
|
||
** `defineLoader`, `defineAction`, `defineClientLoader`, `defineClientAction` ** | ||
|
||
To get typesafety when defining loaders and actions, you can use the `defineLoader` and `defineAction` utilities: | ||
|
||
```ts | ||
import { defineLoader } from "@remix-run/node"; | ||
|
||
export const loader = defineLoader(({ request }) => { | ||
// ^? Request | ||
}); | ||
|
||
export const action = defineAction(({ context }) => { | ||
// ^? AppLoadContext | ||
}); | ||
``` | ||
|
||
Not only does this give you types for any arguments, but it also ensures you are returning single-fetch compatible types: | ||
|
||
```ts | ||
export const loader = defineLoader(() => { | ||
return { hello: "world", badData: () => 1 }; | ||
// ^^^^^^^ Type error: `badData` is not serializable | ||
}); | ||
|
||
export const action = defineAction(() => { | ||
return { hello: "world", badData: new CustomType() }; | ||
// ^^^^^^^ Type error: `badData` is not serializable | ||
}); | ||
``` | ||
|
||
Single-fetch supports the following return types: | ||
|
||
```ts | ||
type Serializable = | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this type exported somewhere? |
||
| undefined | ||
| null | ||
| boolean | ||
| string | ||
| symbol | ||
| number | ||
| bigint | ||
| Date | ||
| URL | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could URLSearchParams be added to this too? I remember someone reporting in Discord that they tried to return this and it failed. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We're using Jacob's turbo-stream library under-the-hood so support for |
||
| RegExp | ||
| Error | ||
| Array<Serializable> | ||
| { [key: PropertyKey]: Serializable } // objects with serializable values | ||
| Map<Serializable, Serializable> | ||
| Set<Serializable> | ||
| Promise<Serializable>; | ||
``` | ||
|
||
**`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: | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What if CustomType class has a toJSON? Couldn't the
badData.toJSON()
method be called and then return as a plain JSON? So you will not get a CustomType client-side but at least the data.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The contract for single-fetch (and
turbo-stream
which is what it is using under-the-hood) is that a typeT
in becomes a typeT
out. SoCustomType
->string
is not supported. If you want you can manually calltoJSON()
:The point is to have less coercion and magic. Having the serialization be basically a glorified ID function is the thing that unlocks a bunch of the simplified types features. Plus it means we can support recursive types much better than the current approach.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sergiodxa FIY, I raised the same question on Discord: https://discord.com/channels/770287896669978684/1233404747885576293/1233417203693523104
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Cross posting my response from discord as well for visibility
I.e., if we add
toJson
support now, folks will start relying on it. Then if we incorporate RSC and it doens't supporttoJSON
, it would be a breaking change for your apps. So we're keeping this simpler now until we know in greater detail how the RSC story shakes out.