Skip to content

Commit c64532c

Browse files
committed
Render the buy server action result as a promise
This is now possible because facebook/react#25634 got merged.
1 parent dfa61e2 commit c64532c

File tree

4 files changed

+45
-62
lines changed

4 files changed

+45
-62
lines changed

src/components/client/buy-button.tsx

Lines changed: 11 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
'use client';
22

3-
import {clsx} from 'clsx';
43
import * as React from 'react';
54
import {useEphemeralState} from '../../hooks/use-ephemeral-state.js';
65
import type {buy} from '../../server-actions/buy.js';
@@ -9,37 +8,24 @@ export interface BuyButtonProps {
98
readonly buy: typeof buy;
109
}
1110

12-
interface Result {
13-
readonly status: 'success' | 'error';
14-
readonly message: React.ReactNode;
15-
}
16-
1711
export function BuyButton({buy}: BuyButtonProps): JSX.Element {
1812
const [quantity, setQuantity] = React.useState(1);
19-
const [isPending, setIsPending] = React.useState(false);
20-
const [result, setResult] = useEphemeralState<Result>(undefined, 3000);
13+
const [isPending, startTransition] = React.useTransition();
2114

22-
const handleClick = async () => {
23-
setIsPending(true);
15+
const [result, setResult] = useEphemeralState<Promise<React.ReactNode>>(
16+
undefined,
17+
3000,
18+
);
2419

25-
try {
26-
const {message, printInnerWidth} = await buy(quantity);
27-
setResult({status: `success`, message});
28-
printInnerWidth();
29-
} catch (error) {
30-
setResult({
31-
status: `error`,
32-
message: isErrorWithDigest(error) ? error.digest : `Unknown Error`,
33-
});
34-
} finally {
35-
setIsPending(false);
36-
}
20+
const handleClick = () => {
21+
startTransition(() => setResult(buy(quantity)));
3722
};
3823

3924
return (
4025
<div>
4126
<p className="my-2">
42-
This is a client component that triggers a server action.
27+
This is a client component that triggers a server action, which in turn
28+
responds with serialized React element that's rendered below the button.
4329
</p>
4430
<input
4531
type="number"
@@ -59,20 +45,8 @@ export function BuyButton({buy}: BuyButtonProps): JSX.Element {
5945
>
6046
Buy now
6147
</button>
62-
{result && (
63-
<p
64-
className={clsx(
65-
`my-2`,
66-
result.status === `success` ? `text-cyan-600` : `text-red-600`,
67-
)}
68-
>
69-
{result.message}
70-
</p>
71-
)}
48+
{/* Promises can now be rendered directly. */}
49+
{result as React.ReactNode}
7250
</div>
7351
);
7452
}
75-
76-
function isErrorWithDigest(error: unknown): error is Error & {digest: string} {
77-
return error instanceof Error && `digest` in error;
78-
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import {clsx} from 'clsx';
2+
import * as React from 'react';
3+
4+
export type NotificationProps = React.PropsWithChildren<{
5+
readonly status: `success` | `error`;
6+
}>;
7+
8+
export function Notification({
9+
children,
10+
status,
11+
}: NotificationProps): JSX.Element {
12+
return (
13+
<p
14+
className={clsx(
15+
`my-2`,
16+
status === `success` ? `text-cyan-600` : `text-red-600`,
17+
)}
18+
>
19+
{children}
20+
</p>
21+
);
22+
}

src/server-actions/buy.tsx

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,26 @@
11
'use server';
22

33
import * as React from 'react';
4-
import {printInnerWidth} from '../client-functions/print-inner-width.js';
4+
import {Notification} from '../components/shared/notification.js';
55

6-
export interface BuyResult {
7-
readonly message: React.ReactNode;
8-
readonly printInnerWidth: () => void;
9-
}
10-
11-
export async function buy(quantity: number): Promise<BuyResult> {
6+
export async function buy(quantity: number): Promise<React.ReactNode> {
127
const itemOrItems = quantity === 1 ? `item` : `items`;
138

149
try {
1510
await new Promise((resolve, reject) =>
1611
setTimeout(Math.random() > 0.2 ? resolve : reject, 500),
1712
);
1813

19-
return {
20-
message: (
21-
<span>
22-
Bought <strong>{quantity}</strong> {itemOrItems}.
23-
</span>
24-
),
25-
printInnerWidth,
26-
};
14+
return (
15+
<Notification status="success">
16+
Bought <strong>{quantity}</strong> {itemOrItems}.
17+
</Notification>
18+
);
2719
} catch {
28-
throw new Error(`Could not buy ${quantity} ${itemOrItems}, try again.`);
20+
return (
21+
<Notification status="error">
22+
Could not buy <strong>{quantity}</strong> {itemOrItems}, try again.
23+
</Notification>
24+
);
2925
}
3026
}

src/workers/rsc/index.tsx

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -81,15 +81,6 @@ const handlePost: ExportedHandlerFetchHandler<EnvWithStaticContent> = async (
8181
const rscStream = ReactServerDOMServer.renderToReadableStream(
8282
actionPromise,
8383
reactClientManifest as ClientManifest,
84-
{
85-
onError: (error) => {
86-
console.error(error);
87-
88-
// TODO: Sending the error message as digest kind of defeats the purpose
89-
// of having a digest to mask the error in production.
90-
return error instanceof Error ? error.message : `Unknown Error`;
91-
},
92-
},
9384
);
9485

9586
return new Response(rscStream, {

0 commit comments

Comments
 (0)