Skip to content
This repository was archived by the owner on Feb 11, 2025. It is now read-only.

Commit

Permalink
feat(next): experimental server actions (trpc#4359)
Browse files Browse the repository at this point in the history
  • Loading branch information
KATT authored May 19, 2023
1 parent b8ec330 commit 930b6fa
Show file tree
Hide file tree
Showing 54 changed files with 1,833 additions and 173 deletions.
1 change: 1 addition & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ comment: false # do not comment PR with the result
ignore:
- '**/deprecated/**'
- '**/form-data/**'
- '**/app-dir/**'
coverage:
range: 50..90 # coverage lower than 50 is red, higher than 90 green, between color code

Expand Down
4 changes: 3 additions & 1 deletion examples/.experimental/next-app-dir/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ This is a playground repo for an offical tRPC + Next.js App directory
- [ ] Implement cache invalidation on client calls
- [ ] Get community feedback
- [ ] Make server calls invalidate client calls and vice verse
- [ ] Proof of concept of server actions
- [x] Proof of concept of server actions
- [ ] Test it heavily
- [ ] Remove codecov ignore
- [ ] Delete all fixme/todo comments
- [ ] Finalize API

### Contributing
Expand Down
3 changes: 3 additions & 0 deletions examples/.experimental/next-app-dir/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"lint": "next lint"
},
"dependencies": {
"@hookform/resolvers": "^2.9.11",
"@tanstack/react-query": "^4.18.0",
"@trpc/client": "^10.26.0",
"@trpc/next": "^10.26.0",
Expand All @@ -20,6 +21,8 @@
"next": "^13.4.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-hook-form": "^7.43.3",
"superjson": "^1.7.4",
"trpc-api": "link:./src/trpc",
"typescript": "^4.8.3",
"zod": "^3.0.0"
Expand Down
25 changes: 24 additions & 1 deletion examples/.experimental/next-app-dir/src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,30 @@ export default function RootLayout({
}) {
return (
<html lang="en">
<body>{children}</body>
<body
style={{
background: 'hsla(216, 28%, 7%, 1)',
color: 'hsla(210, 16%, 80%, 1)',
}}
>
<main
style={{
padding: '3rem',
fontSize: '1.1rem',
}}
>
<div
style={{
padding: '1rem',
background: 'hsla(218, 18%, 12%, 1)',
borderRadius: '0.5rem',
color: 'hsla(210, 16%, 80%, 1)',
}}
>
{children}
</div>
</main>
</body>
</html>
);
}
78 changes: 27 additions & 51 deletions examples/.experimental/next-app-dir/src/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,58 +1,34 @@
import { Suspense } from 'react';
import { api } from 'trpc-api';
import { ClientGreeting } from './ClientGreeting';
import { ServerGreeting } from './ServerGreeting';

export default async function Home() {
const promise = new Promise(async (resolve) => {
await new Promise((r) => setTimeout(r, 1000)); // wait for demo purposes
resolve(api.greeting.query({ text: 'streamed server data' }));
});
import Link from 'next/link';

export default function Index() {
return (
<main
<ul
style={{
width: '100vw',
height: '100vh',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
fontSize: '1.1rem',
listStyle: 'disc',
listStylePosition: 'inside',
padding: 0,
}}
>
<div
style={{
width: '12rem',
padding: '1rem',
background: '#e5e5e5',
borderRadius: '0.5rem',
}}
>
<div>
<Suspense fallback={<>Loading client...</>}>
<ClientGreeting />
</Suspense>
</div>

<div>
<Suspense fallback={<>Loading Server...</>}>
{/* @ts-expect-error RSC + TS not friends yet */}
<ServerGreeting />
</Suspense>
</div>
<div>
<Suspense fallback={<>Loading stream...</>}>
{/** @ts-expect-error - Async Server Component */}
<StreamedSC promise={promise} />
</Suspense>
</div>
</div>
</main>
<li>
<Link
href="/rsc"
style={{
color: 'hsla(210, 16%, 80%, 1)',
}}
>
React Server Components
</Link>
</li>
<li>
<Link
href="/server-action"
style={{
color: 'hsla(210, 16%, 80%, 1)',
}}
>
Server Action
</Link>
</li>
</ul>
);
}

async function StreamedSC(props: { promise: Promise<string> }) {
const data = await props.promise;

return <div>{data}</div>;
}
41 changes: 41 additions & 0 deletions examples/.experimental/next-app-dir/src/app/rsc/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Suspense } from 'react';
import { api } from 'trpc-api';
import { TestMutation } from '../TestMutation';
import { ClientGreeting } from './ClientGreeting';
import { ServerGreeting } from './ServerGreeting';

export default async function Home() {
const promise = new Promise(async (resolve) => {
await new Promise((r) => setTimeout(r, 1000)); // wait for demo purposes
resolve(api.greeting.query({ text: 'streamed server data' }));
});

return (
<>
<div>
<Suspense fallback={<>Loading client...</>}>
<ClientGreeting />
</Suspense>
</div>

<div>
<Suspense fallback={<>Loading Server...</>}>
{/* @ts-expect-error RSC + TS not friends yet */}
<ServerGreeting />
</Suspense>
</div>
<div>
<Suspense fallback={<>Loading stream...</>}>
{/** @ts-expect-error - Async Server Component */}
<StreamedSC promise={promise} />
</Suspense>
</div>
</>
);
}

async function StreamedSC(props: { promise: Promise<string> }) {
const data = await props.promise;

return <div>{data}</div>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
'use client';

import { useAction } from 'trpc-api';
import { testAction } from './_actions';

export function FormWithUseActionExample() {
const mutation = useAction(testAction);
return (
<>
<p>Check the console for the logger output.</p>
<form
action={testAction}
onSubmit={(e) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
mutation.mutate(formData);
}}
>
<input type="text" name="text" />
<button type="submit">Run server action raw debugging</button>

<pre
style={{
overflowX: 'scroll',
}}
>
{JSON.stringify(mutation, null, 4)}
</pre>
</form>
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
'use client';

import { useState } from 'react';
import { testAction } from './_actions';

export function RawExample() {
const [text, setText] = useState('');

return (
<>
<label>
Text to send: <br />
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
/>
</label>
<br />
<button
onClick={async () => {
const res = await testAction({
text,
});
console.log('result', res);
// ^?
if ('result' in res) {
res.result;
res.result.data;
// ^?
} else {
res.error;
// ^?
}
alert('Check console');
}}
>
Run server action raw debugging
</button>
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
'use client';

import { testAction } from './_actions';

export function RawFormExample() {
return (
<>
<p>
Check the network tab and the server console to see that we called this.
If you don not pass an input, it will fail validation and not reach the
procedure.
</p>
<form action={testAction}>
<input type="text" name="text" />
<button type="submit">Run server action raw debugging</button>
</form>
</>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
'use server';

import { z } from 'zod';
import { createAction, publicProcedure } from '~/server/trpc';
import { rhfActionSchema } from './ReactHookFormExample.schema';

/**
* Either inline procedures using trpc's flexible
* builder api, with input parsers and middleware
* Wrap the procedure in a `createAction` call to
* make it server-action friendly
*/
export const rhfAction = createAction(
publicProcedure.input(rhfActionSchema).mutation(async (opts) => {
console.log('testMutation called', opts);
return {
text: `Hello ${opts.input.text}`,
date: new Date(),
};
}),
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { z } from 'zod';

export const rhfActionSchema = z.object({
text: z.string().min(1),
});
Loading

0 comments on commit 930b6fa

Please sign in to comment.