Skip to content

Commit

Permalink
Adjust home page to demo server action queue
Browse files Browse the repository at this point in the history
  • Loading branch information
unstubbable committed Apr 12, 2024
1 parent 42f3ba6 commit a4c3890
Show file tree
Hide file tree
Showing 5 changed files with 54 additions and 46 deletions.
4 changes: 1 addition & 3 deletions apps/cloudflare-app/src/worker/worker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,6 @@ describe(`worker`, () => {
const resp = await worker.fetch();
const text = await resp.text();

expect(text).toMatch(
`<p class="my-3">This is a suspended server component.</p>`,
);
expect(text).toMatch(`Hello, <em>World</em>!`);
});
});
2 changes: 0 additions & 2 deletions apps/shared-app/src/client/button.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
'use client';

import * as React from 'react';
import {trackClick} from '../server/track-click.js';

export type ButtonProps = React.PropsWithChildren<{
readonly disabled?: boolean;
Expand All @@ -10,7 +9,6 @@ export type ButtonProps = React.PropsWithChildren<{
export function Button({children, disabled}: ButtonProps): React.ReactNode {
return (
<button
onClick={() => void trackClick()}
disabled={disabled}
className="rounded-full bg-cyan-500 py-1 px-4 text-white disabled:bg-zinc-300"
>
Expand Down
77 changes: 48 additions & 29 deletions apps/shared-app/src/client/product.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,43 +7,62 @@ import {Notification} from '../shared/notification.js';
import {Button} from './button.js';

export interface ProductProps {
readonly name: string;
readonly buy: (
prevResult: BuyResult | undefined,
formData: FormData,
) => Promise<BuyResult>;
}

export function Product({buy}: ProductProps): React.ReactNode {
const [result, formAction, isPending] = React.useActionState(buy, undefined);
export function Product({name, buy}: ProductProps): React.ReactNode {
const [formState, formAction] = React.useActionState(buy, undefined);

const [result, setOptimisticResult] = React.useOptimistic<
BuyResult | undefined,
number
>(formState, (prevResult, quantity) => ({
status: `success`,
quantity,
totalQuantityInSession:
(prevResult?.totalQuantityInSession ?? 0) + quantity,
}));

const handleSubmit: React.FormEventHandler<HTMLFormElement> = (event) => {
const formData = new FormData(event.currentTarget);
event.preventDefault();

React.startTransition(() => {
setOptimisticResult(parseInt(formData.get(`quantity`) as string, 10));
formAction(formData);
});
};

return (
<form action={formAction}>
<p className="my-2">
This is a client component that renders a form with a form action. On
submit, a server action is called with the current form data, which in
turn responds with a success or error result.
</p>
<p className="my-2">
The form submission also works before hydration, including server-side
rendering of the result! This can be simulated by blocking the
javascript files.
</p>
<input
type="number"
name="quantity"
defaultValue={1}
step={1}
min={1}
max={99}
className={clsx(
`p-1`,
result?.status === `error` && result.fieldErrors?.quantity
? [`bg-red-100`, `outline-red-700`]
: [`bg-zinc-100`, `outline-cyan-500`],
)}
/>
{` `}
<Button disabled={isPending}>Buy now</Button>
<form
action={formAction}
onSubmit={handleSubmit}
className="flex items-end gap-x-3"
>
<div className="space-y-2">
<h3 className="font-semibold">{name}</h3>
<div className="space-x-2">
<input
type="number"
name="quantity"
defaultValue={1}
step={1}
min={1}
max={99}
className={clsx(
`p-1`,
result?.status === `error` && result.fieldErrors?.quantity
? [`bg-red-100`, `outline-red-700`]
: [`bg-zinc-100`, `outline-cyan-500`],
)}
/>
<Button>Buy now</Button>
</div>
</div>
{result && (
<Notification status={result.status}>
{result.status === `success` ? (
Expand Down
9 changes: 4 additions & 5 deletions apps/shared-app/src/server/home-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,16 @@ import {Product} from '../client/product.js';
import {Main} from '../shared/main.js';
import {buy} from './buy.js';
import {Hello} from './hello.js';
import {Suspended} from './suspended.js';

export function HomePage(): React.ReactNode {
return (
<Main>
<Hello />
<React.Suspense fallback={<p className="my-3">Loading...</p>}>
<Suspended />
</React.Suspense>
<React.Suspense>
<Product buy={buy.bind(null, `some-product-id`)} />
<div className="space-y-3">
<Product name="Product A" buy={buy.bind(null, `a`)} />
<Product name="Product B" buy={buy.bind(null, `b`)} />
</div>
</React.Suspense>
</Main>
);
Expand Down
8 changes: 1 addition & 7 deletions apps/shared-app/src/shared/notification.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import {clsx} from 'clsx';
import * as React from 'react';

export type NotificationProps = React.PropsWithChildren<{
Expand All @@ -10,12 +9,7 @@ export function Notification({
status,
}: NotificationProps): React.ReactNode {
return (
<div
className={clsx(
`my-2`,
status === `success` ? `text-cyan-600` : `text-red-600`,
)}
>
<div className={status === `success` ? `text-cyan-600` : `text-red-600`}>
{children}
</div>
);
Expand Down

0 comments on commit a4c3890

Please sign in to comment.