Skip to content

Commit

Permalink
Render the buy server action result as a promise
Browse files Browse the repository at this point in the history
This is now possible because facebook/react#25634 got merged.
  • Loading branch information
unstubbable committed Mar 14, 2023
1 parent dfa61e2 commit c64532c
Show file tree
Hide file tree
Showing 4 changed files with 45 additions and 62 deletions.
48 changes: 11 additions & 37 deletions src/components/client/buy-button.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
'use client';

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

interface Result {
readonly status: 'success' | 'error';
readonly message: React.ReactNode;
}

export function BuyButton({buy}: BuyButtonProps): JSX.Element {
const [quantity, setQuantity] = React.useState(1);
const [isPending, setIsPending] = React.useState(false);
const [result, setResult] = useEphemeralState<Result>(undefined, 3000);
const [isPending, startTransition] = React.useTransition();

const handleClick = async () => {
setIsPending(true);
const [result, setResult] = useEphemeralState<Promise<React.ReactNode>>(
undefined,
3000,
);

try {
const {message, printInnerWidth} = await buy(quantity);
setResult({status: `success`, message});
printInnerWidth();
} catch (error) {
setResult({
status: `error`,
message: isErrorWithDigest(error) ? error.digest : `Unknown Error`,
});
} finally {
setIsPending(false);
}
const handleClick = () => {
startTransition(() => setResult(buy(quantity)));
};

return (
<div>
<p className="my-2">
This is a client component that triggers a server action.
This is a client component that triggers a server action, which in turn
responds with serialized React element that's rendered below the button.
</p>
<input
type="number"
Expand All @@ -59,20 +45,8 @@ export function BuyButton({buy}: BuyButtonProps): JSX.Element {
>
Buy now
</button>
{result && (
<p
className={clsx(
`my-2`,
result.status === `success` ? `text-cyan-600` : `text-red-600`,
)}
>
{result.message}
</p>
)}
{/* Promises can now be rendered directly. */}
{result as React.ReactNode}
</div>
);
}

function isErrorWithDigest(error: unknown): error is Error & {digest: string} {
return error instanceof Error && `digest` in error;
}
22 changes: 22 additions & 0 deletions src/components/shared/notification.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import {clsx} from 'clsx';
import * as React from 'react';

export type NotificationProps = React.PropsWithChildren<{
readonly status: `success` | `error`;
}>;

export function Notification({
children,
status,
}: NotificationProps): JSX.Element {
return (
<p
className={clsx(
`my-2`,
status === `success` ? `text-cyan-600` : `text-red-600`,
)}
>
{children}
</p>
);
}
28 changes: 12 additions & 16 deletions src/server-actions/buy.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,26 @@
'use server';

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

export interface BuyResult {
readonly message: React.ReactNode;
readonly printInnerWidth: () => void;
}

export async function buy(quantity: number): Promise<BuyResult> {
export async function buy(quantity: number): Promise<React.ReactNode> {
const itemOrItems = quantity === 1 ? `item` : `items`;

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

return {
message: (
<span>
Bought <strong>{quantity}</strong> {itemOrItems}.
</span>
),
printInnerWidth,
};
return (
<Notification status="success">
Bought <strong>{quantity}</strong> {itemOrItems}.
</Notification>
);
} catch {
throw new Error(`Could not buy ${quantity} ${itemOrItems}, try again.`);
return (
<Notification status="error">
Could not buy <strong>{quantity}</strong> {itemOrItems}, try again.
</Notification>
);
}
}
9 changes: 0 additions & 9 deletions src/workers/rsc/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,15 +81,6 @@ const handlePost: ExportedHandlerFetchHandler<EnvWithStaticContent> = async (
const rscStream = ReactServerDOMServer.renderToReadableStream(
actionPromise,
reactClientManifest as ClientManifest,
{
onError: (error) => {
console.error(error);

// TODO: Sending the error message as digest kind of defeats the purpose
// of having a digest to mask the error in production.
return error instanceof Error ? error.message : `Unknown Error`;
},
},
);

return new Response(rscStream, {
Expand Down

0 comments on commit c64532c

Please sign in to comment.