Skip to content

Commit

Permalink
Merge pull request #22 from chungweileong94/react-19
Browse files Browse the repository at this point in the history
Better React 19 support
  • Loading branch information
chungweileong94 authored May 18, 2024
2 parents d20512f + 53d3860 commit 20298bc
Show file tree
Hide file tree
Showing 10 changed files with 445 additions and 152 deletions.
9 changes: 9 additions & 0 deletions .changeset/little-ravens-boil.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"server-act": minor
---

Better React 19 support

- Updated `useFormState` example to `useActionState`.
- `prevState` from form action is now `undefined` type by default.
- You can now access `formData` in form action.
14 changes: 7 additions & 7 deletions examples/nextjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,20 @@
"typecheck": "tsc --noEmit"
},
"dependencies": {
"next": "14.2.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"next": "14.3.0-canary.70",
"react": "19.0.0-beta-04b058868c-20240508",
"react-dom": "19.0.0-beta-04b058868c-20240508",
"server-act": "workspace:*",
"zod": "^3.22.2",
"zod-form-data": "^2.0.2"
},
"devDependencies": {
"@types/node": "20.6.3",
"@types/react": "18.2.33",
"@types/react-dom": "18.2.14",
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"autoprefixer": "10.4.15",
"postcss": "8.4.30",
"tailwindcss": "3.3.3",
"typescript": "^5.2.2"
"typescript": "^5.4.5"
}
}
6 changes: 3 additions & 3 deletions examples/nextjs/src/app/form-action/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ export const sayHelloAction = serverAct
name: zfd.text(
z
.string({ required_error: `You haven't told me your name` })
.nonempty({ message: "You need to tell me your name!" }),
.max(20, { message: "Any shorter name? You name is too long 😬" }),
),
}),
)
.formAction(async ({ input, formErrors, ctx }) => {
.formAction(async ({ formData, input, formErrors, ctx }) => {
if (formErrors) {
return { formErrors: formErrors.formErrors.fieldErrors };
return { formData, formErrors: formErrors.formErrors.fieldErrors };
}

console.log(
Expand Down
11 changes: 6 additions & 5 deletions examples/nextjs/src/app/form-action/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import { useFormState, useFormStatus } from "react-dom";

import { useActionState } from "react";
import { useFormStatus } from "react-dom";
import { sayHelloAction } from "./actions";

function SubmitButton() {
Expand All @@ -18,7 +18,7 @@ function SubmitButton() {
}

export default function FormAction() {
const [state, dispatch] = useFormState(sayHelloAction, { formErrors: {} });
const [state, dispatch] = useActionState(sayHelloAction, undefined);

return (
<main className="flex min-h-screen flex-col items-center justify-center p-24">
Expand All @@ -31,10 +31,11 @@ export default function FormAction() {
id="name"
name="name"
className="rounded-md border-2 border-black px-4 py-2"
defaultValue={state?.formData?.get("name")?.toString()}
/>
<SubmitButton />
{state.message && <p className="text-gray-500">{state.message}</p>}
{state.formErrors?.name?.map((error) => (
{state?.message && <p className="text-gray-500">{state.message}</p>}
{state?.formErrors?.name?.map((error) => (
<p key={error} className="text-red-500">
{error}
</p>
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"@changesets/changelog-github": "^0.4.8",
"@changesets/cli": "^2.26.2",
"turbo": "^1.13.3",
"typescript": "^5.2.2",
"typescript": "^5.4.5",
"vitest": "^1.6.0"
},
"packageManager": "pnpm@8.15.7"
Expand Down
26 changes: 15 additions & 11 deletions packages/server-act/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,12 +82,11 @@ export const sayHelloAction = serverAct
});
```

### `useFormState` Support
### `useActionState` Support

> `useFormState` Documentation:
> `useActionState` Documentation:
>
> - https://nextjs.org/docs/app/building-your-application/data-fetching/forms-and-mutations#error-handling
> - https://react.dev/reference/react-dom/hooks/useFormState
> - https://react.dev/reference/react/useActionState
We recommend using [zod-form-data](https://www.npmjs.com/package/zod-form-data) for input validation.

Expand All @@ -105,13 +104,13 @@ export const sayHelloAction = serverAct
name: zfd.text(
z
.string({ required_error: `You haven't told me your name` })
.nonempty({ message: 'You need to tell me your name!' }),
.max(20, { message: "Any shorter name? You name is too long 😬" }),
),
}),
)
.formAction(async ({ input, formErrors, ctx }) => {
.formAction(async ({ formData, input, formErrors, ctx }) => {
if (formErrors) {
return { formErrors: formErrors.formErrors.fieldErrors };
return { formData, formErrors: formErrors.formErrors.fieldErrors };
}
return { message: `Hello, ${input.name}!` };
});
Expand All @@ -121,19 +120,24 @@ export const sayHelloAction = serverAct
// client-component.tsx
"use client";

import { useActionState } from "react";
import { sayHelloAction } from "./action";

export const ClientComponent = () => {
const [state, dispatch] = useFormState(sayHelloAction, { formErrors: {} });
const [state, dispatch] = useFormState(sayHelloAction, undefined);

return (
<form action={dispatch}>
<input name="name" required />
{state.formErrors?.name?.map((error) => <p key={error}>{error}</p>)}
<input
name="name"
required
defaultValue={state?.formData?.get("name")?.toString()}
/>
{state?.formErrors?.name?.map((error) => <p key={error}>{error}</p>)}

<button type="submit">Submit</button>

{!!state.message && <p>{state.message}</p>}
{!!state?.message && <p>{state.message}</p>}
</form>
);
};
Expand Down
4 changes: 2 additions & 2 deletions packages/server-act/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,12 @@
],
"devDependencies": {
"bunchee": "^5.1.2",
"typescript": "^5.2.2",
"typescript": "^5.4.5",
"zod": "^3.22.2",
"zod-form-data": "^2.0.2"
},
"peerDependencies": {
"typescript": "^5.2.2",
"typescript": ">=5.0.0",
"zod": "^3.22.2"
},
"peerDependenciesMeta": {
Expand Down
23 changes: 16 additions & 7 deletions packages/server-act/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,14 +103,17 @@ describe("formAction", () => {
const action = serverAct.formAction(async () => Promise.resolve("bar"));

expectTypeOf(action).toEqualTypeOf<
(prevState: string, formData: FormData) => Promise<string>
(
prevState: string | undefined,
formData: FormData,
) => Promise<string | undefined>
>();
expectTypeOf(action).parameter(0).toBeString();
expectTypeOf(action).parameter(0).toEqualTypeOf<string | undefined>();
expectTypeOf(action).parameter(1).toHaveProperty("append");
expectTypeOf(action).parameter(1).toHaveProperty("delete");
expectTypeOf(action).parameter(1).toHaveProperty("get");
expectTypeOf(action).parameter(1).toHaveProperty("entries");
expectTypeOf(action).returns.resolves.toBeString();
expectTypeOf(action).returns.resolves.toEqualTypeOf<string | undefined>();

expect(action.constructor.name).toBe("AsyncFunction");

Expand All @@ -124,14 +127,17 @@ describe("formAction", () => {
.formAction(async () => Promise.resolve("bar"));

expectTypeOf(action).toEqualTypeOf<
(prevState: string, formData: FormData) => Promise<string>
(
prevState: string | undefined,
formData: FormData,
) => Promise<string | undefined>
>();
expectTypeOf(action).parameter(0).toBeString();
expectTypeOf(action).parameter(0).toEqualTypeOf<string | undefined>();
expectTypeOf(action).parameter(1).toHaveProperty("append");
expectTypeOf(action).parameter(1).toHaveProperty("delete");
expectTypeOf(action).parameter(1).toHaveProperty("get");
expectTypeOf(action).parameter(1).toHaveProperty("entries");
expectTypeOf(action).returns.resolves.toBeString();
expectTypeOf(action).returns.resolves.toEqualTypeOf<string | undefined>();

expect(action.constructor.name).toBe("AsyncFunction");

Expand All @@ -156,7 +162,10 @@ describe("formAction", () => {

type State = string | z.ZodError<{ foo: string }>;
expectTypeOf(action).toEqualTypeOf<
(prevState: State, formData: FormData) => Promise<State>
(
prevState: State | undefined,
formData: FormData,
) => Promise<State | undefined>
>();
expectTypeOf(action).parameter(1).toHaveProperty("append");
expectTypeOf(action).parameter(1).toHaveProperty("delete");
Expand Down
23 changes: 16 additions & 7 deletions packages/server-act/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,14 @@ interface ActionBuilder<TParams extends ActionParams> {
/**
* Create an action for React `useFormState`
*/
formAction: <TState>(
formAction: <TState, TPrevState = undefined>(
action: (
params: Prettify<
{
ctx: InferContextType<TParams["_context"]>;
// biome-ignore lint/suspicious/noExplicitAny: Intended
prevState: any; // FIXME: This supposes to be `TState`, but we can't, as it will break the type.
// biome-ignore lint/suspicious/noExplicitAny: FIXME: This supposes to be `TState`, but we can't, as it will break the type.
prevState: any;
formData: FormData;
} & (
| {
input: InferInputType<TParams["_input"], "out">;
Expand All @@ -93,7 +94,10 @@ interface ActionBuilder<TParams extends ActionParams> {
)
>,
) => Promise<TState>,
) => (prevState: TState, formData: FormData) => Promise<TState>;
) => (
prevState: TState | TPrevState,
formData: FormData,
) => Promise<TState | TPrevState>;
}
// biome-ignore lint/suspicious/noExplicitAny: Intended
type AnyActionBuilder = ActionBuilder<any>;
Expand Down Expand Up @@ -151,11 +155,16 @@ function createServerActionBuilder(
if (_def.input) {
const result = await _def.input.safeParseAsync(formData);
if (!result.success) {
return await action({ ctx, prevState, formErrors: result.error });
return await action({
ctx,
prevState,
formData,
formErrors: result.error,
});
}
return await action({ ctx, prevState, input: result.data });
return await action({ ctx, prevState, formData, input: result.data });
}
return await action({ ctx, prevState, input: undefined });
return await action({ ctx, prevState, formData, input: undefined });
};
},
};
Expand Down
Loading

0 comments on commit 20298bc

Please sign in to comment.