This repository was archived by the owner on Feb 11, 2025. It is now read-only.
forked from trpc/trpc
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add experimental support for
FormData
mutations (trpc#3795)
- Loading branch information
1 parent
c8caca9
commit 0aaf147
Showing
61 changed files
with
6,292 additions
and
12,514 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,5 @@ | ||
public-hoist-pattern[]=@types/* | ||
public-hoist-pattern[]=playwright-core | ||
public-hoist-pattern[]=zod | ||
side-effects-cache=false | ||
strict-peer-dependencies=false |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
public/uploads |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
# Next.js + tRPC + `FormData` | ||
|
||
> 🚧🚧 🚧 🚧 🚧 🚧 🚧 🚧 🚧 This is experimental 🚧 🚧 🚧 🚧 🚧 🚧 🚧 🚧 🚧 | ||
## Setup | ||
|
||
```bash | ||
npx create-next-app --example https://github.com/trpc/trpc --example-path examples/.experimental/next-formdata trpc-formdata | ||
cd trpc-formdata | ||
npm i | ||
npm run dev | ||
``` | ||
|
||
### Adding FormData to your project | ||
|
||
1. Add support for FormData & JSON content types to your HTTP handler: | ||
|
||
- https://github.com/trpc/trpc/blob/5d234b6d26173256caa16045cf3ba931399c2629/examples/.experimental/next-formdata/src/pages/api/trpc/%5Btrpc%5D.ts#L6-L7 | ||
- https://github.com/trpc/trpc/blob/5d234b6d26173256caa16045cf3ba931399c2629/examples/.experimental/next-formdata/src/pages/api/trpc/%5Btrpc%5D.ts#L24-L27 | ||
|
||
1. Make sure you return `req` in your `createContext()`-fn: https://github.com/trpc/trpc/blob/524b0c866b55cfc1ddd0f89885b7a5f26dde288a/examples/.experimental/next-formdata/src/server/trpc.ts#L20 | ||
1. Add a middleware where you want to use `FormData`: https://github.com/trpc/trpc/blob/1b29a7425ef784c59b2c5e3bc1229713671508d6/examples/.experimental/next-formdata/src/server/routers/room.ts#L11-L21 | ||
1. Add a `httpFormDataLink`: https://github.com/trpc/trpc/blob/74bb462ca9ba91e8e077f5abf655c792b87f6995/examples/.experimental/next-formdata/src/utils/trpc.ts#L57-L65 | ||
1. Create a validation schema using `FormData`: https://github.com/trpc/trpc/blob/d1b3d5b53ff54af5d00ab99052ecf23a84277635/examples/.experimental/next-formdata/src/utils/schemas.ts#L1-L11 | ||
1. Write a backend procedure that uses the form data: https://github.com/trpc/trpc/blob/d1b3d5b53ff54af5d00ab99052ecf23a84277635/examples/.experimental/next-formdata/src/server/routers/room.ts#L25-L31 | ||
1. Create a form, see examples: | ||
- [./src/pages/vanilla.tsx](./src/pages/vanilla.tsx) | ||
- [./src/pages/react-hook-form.tsx](./src/pages/react-hook-form.tsx) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
/// <reference types="next" /> | ||
/// <reference types="next/image-types/global" /> | ||
|
||
// NOTE: This file should not be edited | ||
// see https://nextjs.org/docs/basic-features/typescript for more information. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
/** @type {import("next").NextConfig} */ | ||
module.exports = { | ||
/** We run eslint as a separate task in CI */ | ||
eslint: { ignoreDuringBuilds: !!process.env.CI }, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
{ | ||
"name": "@examples/next-formdata", | ||
"version": "10.21.0", | ||
"private": true, | ||
"scripts": { | ||
"dev": "next dev", | ||
"build": "next build", | ||
"lint": "eslint --ext \".js,.ts,.tsx\" --report-unused-disable-directives src", | ||
"start": "next start" | ||
}, | ||
"dependencies": { | ||
"@hookform/error-message": "^2.0.1", | ||
"@hookform/resolvers": "^2.9.11", | ||
"@tanstack/react-query": "^4.18.0", | ||
"@trpc/client": "10.22.0", | ||
"@trpc/next": "10.22.0", | ||
"@trpc/react-query": "10.22.0", | ||
"@trpc/server": "10.22.0", | ||
"next": "^13.2.1", | ||
"react": "^18.2.0", | ||
"react-dom": "^18.2.0", | ||
"react-hook-form": "^7.43.3", | ||
"undici": "^5.14.0", | ||
"zod": "^3.0.0", | ||
"zod-form-data": "^2.0.1" | ||
}, | ||
"devDependencies": { | ||
"@types/node": "^18.7.20", | ||
"@types/react": "^18.0.9", | ||
"@types/react-dom": "^18.0.5", | ||
"eslint": "^8.30.0", | ||
"typescript": "^4.8.3" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
import type { AppType } from 'next/app'; | ||
import { trpc } from '../utils/trpc'; | ||
|
||
const MyApp: AppType = ({ Component, pageProps }) => { | ||
return <Component {...pageProps} />; | ||
}; | ||
|
||
export default trpc.withTRPC(MyApp); |
40 changes: 40 additions & 0 deletions
40
examples/.experimental/next-formdata/src/pages/api/trpc/[trpc].ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
/** | ||
* This is the API-handler of your app that contains all your API routes. | ||
* On a bigger app, you will probably want to split this file up into multiple files. | ||
*/ | ||
import * as trpcNext from '@trpc/server/adapters/next'; | ||
import { nodeHTTPFormDataContentTypeHandler } from '@trpc/server/adapters/node-http/content-type/form-data'; | ||
import { nodeHTTPJSONContentTypeHandler } from '@trpc/server/adapters/node-http/content-type/json'; | ||
import { NextApiRequest, NextApiResponse } from 'next'; | ||
import * as undici from 'undici'; | ||
import { roomRouter } from '~/server/routers/room'; | ||
import { createContext, router } from '~/server/trpc'; | ||
|
||
const appRouter = router({ | ||
room: roomRouter, | ||
}); | ||
|
||
// export only the type definition of the API | ||
// None of the actual implementation is exposed to the client | ||
export type AppRouter = typeof appRouter; | ||
|
||
const handler = trpcNext.createNextApiHandler({ | ||
router: appRouter, | ||
createContext, | ||
experimental_contentTypeHandlers: [ | ||
nodeHTTPFormDataContentTypeHandler(), | ||
nodeHTTPJSONContentTypeHandler(), | ||
], | ||
}); | ||
|
||
// export API handler | ||
export default async (req: NextApiRequest, res: NextApiResponse) => { | ||
await handler(req, res); | ||
}; | ||
|
||
export const config = { | ||
api: { | ||
bodyParser: false, | ||
responseLimit: '100mb', | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,14 @@ | ||
import Link from 'next/link'; | ||
|
||
export default function IndexPage() { | ||
return ( | ||
<ul> | ||
<li> | ||
<Link href="/vanilla">/vanilla</Link> | ||
</li> | ||
<li> | ||
<Link href="/react-hook-form">/react-hook-form</Link> | ||
</li> | ||
</ul> | ||
); | ||
} |
139 changes: 139 additions & 0 deletions
139
examples/.experimental/next-formdata/src/pages/react-hook-form.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,139 @@ | ||
import { zodResolver } from '@hookform/resolvers/zod'; | ||
import { useRef, useState } from 'react'; | ||
import { FormProvider, UseFormProps, useForm } from 'react-hook-form'; | ||
import { z } from 'zod'; | ||
import { uploadFileSchema } from '~/utils/schemas'; | ||
import { trpc } from '~/utils/trpc'; | ||
|
||
/** | ||
* zod-form-data wraps zod in an effect where the original type is a `FormData` | ||
*/ | ||
type UnwrapZodEffect<TType extends z.ZodType> = TType extends z.ZodEffects< | ||
infer U, | ||
any, | ||
any | ||
> | ||
? U | ||
: TType; | ||
|
||
type GetInput<TType extends z.ZodType> = UnwrapZodEffect<TType>['_input']; | ||
|
||
function useZodFormData<TSchema extends z.ZodType>( | ||
props: { | ||
schema: TSchema; | ||
} & Omit<UseFormProps<GetInput<TSchema>>, 'resolver'>, | ||
) { | ||
const formRef = useRef<HTMLFormElement>(null); | ||
const _resolver = zodResolver(props.schema, undefined, { | ||
rawValues: true, | ||
}); | ||
|
||
const form = useForm<GetInput<TSchema>>({ | ||
...props, | ||
resolver: (_, ctx, opts) => { | ||
if (!formRef.current) { | ||
return { | ||
values: {}, | ||
errors: { | ||
root: { | ||
message: 'Form not mounted', | ||
}, | ||
}, | ||
}; | ||
} | ||
const values = new FormData(formRef.current); | ||
return _resolver(values, ctx, opts); | ||
}, | ||
}); | ||
|
||
return { ...form, formRef }; | ||
} | ||
|
||
export default function Page() { | ||
const mutation = trpc.room.sendMessage.useMutation({ | ||
onError(err) { | ||
alert('Error from server: ' + err.message); | ||
}, | ||
}); | ||
|
||
const form = useZodFormData({ | ||
schema: uploadFileSchema, | ||
defaultValues: { | ||
name: 'whadaaaap', | ||
}, | ||
}); | ||
|
||
const [noJs, setNoJs] = useState(false); | ||
|
||
return ( | ||
<> | ||
<h2 className="text-3xl font-bold">Posts</h2> | ||
|
||
<FormProvider {...form}> | ||
<form | ||
method="post" | ||
action={`/api/trpc/${mutation.trpc.path}`} | ||
encType="multipart/form-data" | ||
onSubmit={(_event) => { | ||
if (noJs) { | ||
return; | ||
} | ||
void form.handleSubmit(async (values, event) => { | ||
await mutation.mutateAsync(new FormData(event?.target)); | ||
})(_event); | ||
}} | ||
style={{ display: 'flex', flexDirection: 'column', gap: 10 }} | ||
ref={form.formRef} | ||
> | ||
<fieldset> | ||
<legend>Form with file upload</legend> | ||
<div style={{}}> | ||
<label htmlFor="name">Enter your name</label> | ||
<input {...form.register('name')} /> | ||
{form.formState.errors.name && ( | ||
<div>{form.formState.errors.name.message}</div> | ||
)} | ||
</div> | ||
|
||
<div> | ||
<label>Required file, only images</label> | ||
<input type="file" {...form.register('image')} /> | ||
{form.formState.errors.image && ( | ||
<div>{form.formState.errors.image.message}</div> | ||
)} | ||
</div> | ||
|
||
<div> | ||
<label>Post without JS</label> | ||
<input | ||
type="checkbox" | ||
checked={noJs} | ||
onChange={(e) => setNoJs(e.target.checked)} | ||
/> | ||
</div> | ||
<div> | ||
<button type="submit" disabled={mutation.status === 'loading'}> | ||
submit | ||
</button> | ||
</div> | ||
</fieldset> | ||
</form> | ||
|
||
{mutation.data && ( | ||
<fieldset> | ||
<legend>Upload result</legend> | ||
<ul> | ||
<li> | ||
Image: <br /> | ||
<img | ||
src={mutation.data.image.url} | ||
alt={mutation.data.image.url} | ||
/> | ||
</li> | ||
</ul> | ||
</fieldset> | ||
)} | ||
</FormProvider> | ||
</> | ||
); | ||
} |
Oops, something went wrong.