Skip to content

Commit

Permalink
Use useFormState and useFormStatus
Browse files Browse the repository at this point in the history
  • Loading branch information
unstubbable committed Nov 1, 2023
1 parent 6b19b63 commit 53ea05d
Show file tree
Hide file tree
Showing 5 changed files with 71 additions and 74 deletions.
11 changes: 6 additions & 5 deletions apps/shared-app/src/client/button.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
'use client';

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

export type ButtonProps = React.PropsWithChildren<{
readonly disabled?: boolean;
}>;
export type ButtonProps = React.PropsWithChildren<{}>;

export function Button({children}: ButtonProps): JSX.Element {
const {pending} = ReactDOM.useFormStatus();

export function Button({children, disabled}: ButtonProps): JSX.Element {
return (
<button
onClick={() => void trackClick()}
disabled={disabled}
disabled={pending}
className="rounded-full bg-cyan-500 py-1 px-4 text-white disabled:bg-zinc-300"
>
{children}
Expand Down
48 changes: 24 additions & 24 deletions apps/shared-app/src/client/product.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,20 @@

import {clsx} from 'clsx';
import * as React from 'react';
import type {buy} from '../server/buy.js';
import * as ReactDOM from 'react-dom';
import {buy} from '../server/buy.js';
import {Notification} from '../shared/notification.js';
import {Button} from './button.js';
import {useEphemeralState} from './use-ephemeral-state.js';

export interface ProductProps {
readonly buy: typeof buy;
}

export function Product({buy}: ProductProps): JSX.Element {
const [isPending, startTransition] = React.useTransition();

const [result, setResult] = useEphemeralState<ReturnType<typeof buy>>(
undefined,
5000,
);

const [message, fieldErrors] = result ? React.use(result) : [];

const formAction = (formData: FormData) => {
startTransition(() => setResult(buy(formData)));
};
export function Product(): JSX.Element {
const [result, formAction] = ReactDOM.useFormState(buy, undefined);

return (
<form action={formAction}>
<p className="my-2">
This is a client component that renders a form with a form action. On
submit, the form action calls a server action with the current form
data, which in turn responds with a serialized React element that's
rendered below the button.
data, which in turn responds with a success or error result.
</p>
<input
type="number"
Expand All @@ -41,14 +26,29 @@ export function Product({buy}: ProductProps): JSX.Element {
max={99}
className={clsx(
`p-1`,
fieldErrors?.quantity
result?.status === `error` && result.fieldErrors?.quantity
? [`bg-red-100`, `outline-red-700`]
: [`bg-zinc-100`, `outline-cyan-500`],
)}
/>
{` `}
<Button disabled={isPending}>Buy now</Button>
{message}
<Button>Buy now</Button>
{result && (
<Notification status={result.status}>
{result.status === `success` ? (
<p>
Bought <strong>{result.quantity}</strong>
{` `}
{result.quantity === 1 ? `item` : `items`}.
</p>
) : (
<p>{result.message}</p>
)}
<p>
Total items bought: <strong>{result.totalQuantityInSession}</strong>
</p>
</Notification>
)}
</form>
);
}
26 changes: 0 additions & 26 deletions apps/shared-app/src/client/use-ephemeral-state.ts

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,12 +1,27 @@
'use server';

import * as React from 'react';
import 'server-only';
import {z} from 'zod';
import {Notification} from '../shared/notification.js';
import {wait} from './wait.js';

type BuyFieldErrors = z.inferFlattenedErrors<typeof BuyFormData>['fieldErrors'];
export type BuyResult = BuySuccessResult | BuyErrorResult;

export interface BuySuccessResult {
readonly status: 'success';
readonly quantity: number;
readonly totalQuantityInSession: number;
}

export interface BuyErrorResult {
readonly status: 'error';
readonly message: string;
readonly fieldErrors?: BuyFieldErrors;
readonly totalQuantityInSession: number;
}

export type BuyFieldErrors = z.inferFlattenedErrors<
typeof BuyFormData
>['fieldErrors'];

const FormDataFields = z.instanceof(FormData).transform((formData) => {
const fields: Record<string, string> = {};
Expand Down Expand Up @@ -37,34 +52,42 @@ async function fetchAvailableProductCount(): Promise<number> {
}

export async function buy(
prevResult: BuyResult | undefined,
formData: FormData,
): Promise<[React.ReactNode] | [React.ReactNode, BuyFieldErrors]> {
): Promise<BuyResult> {
const parsedFormData = FormDataFields.safeParse(formData);
const totalQuantityInSession = prevResult?.totalQuantityInSession ?? 0;

if (!parsedFormData.success) {
return [
<Notification status="error">An unexpected error occured.</Notification>,
];
console.error(parsedFormData.error);

return {
status: `error`,
message: `An unexpected error occured.`,
totalQuantityInSession,
};
}

const result = await BuyFormData.safeParseAsync(parsedFormData.data);

if (!result.success) {
const {fieldErrors} = result.error.formErrors;

return [
<Notification status="error">
{Object.values(fieldErrors).flat().join(` `)}
</Notification>,
return {
status: `error`,
message: Object.values(fieldErrors).flat().join(` `),
fieldErrors,
];
totalQuantityInSession,
};
}

const {quantity} = result.data;

return [
<Notification status="success">
Bought <strong>{quantity}</strong> {quantity === 1 ? `item` : `items`}.
</Notification>,
];
// Buy quantity number of items ...

return {
status: `success`,
quantity,
totalQuantityInSession: totalQuantityInSession + quantity,
};
}
3 changes: 1 addition & 2 deletions apps/shared-app/src/server/home-page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import * as React from 'react';
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';

Expand All @@ -13,7 +12,7 @@ export function HomePage(): JSX.Element {
<Suspended />
</React.Suspense>
<React.Suspense>
<Product buy={buy} />
<Product />
</React.Suspense>
</Main>
);
Expand Down

0 comments on commit 53ea05d

Please sign in to comment.