From 6967ae322a6ae36f66df8cfa494d29f47419e2fa Mon Sep 17 00:00:00 2001 From: Ryan Lewis Date: Wed, 29 May 2024 16:47:31 -0700 Subject: [PATCH 1/9] server action example with useFormState --- app/src/app/[locale]/serverAction/page.tsx | 84 +++++++++++++++++++ .../app/serverActions/serverActionExample.ts | 30 +++++++ 2 files changed, 114 insertions(+) create mode 100644 app/src/app/[locale]/serverAction/page.tsx create mode 100644 app/src/app/serverActions/serverActionExample.ts diff --git a/app/src/app/[locale]/serverAction/page.tsx b/app/src/app/[locale]/serverAction/page.tsx new file mode 100644 index 00000000..80031123 --- /dev/null +++ b/app/src/app/[locale]/serverAction/page.tsx @@ -0,0 +1,84 @@ +"use client"; + +import { useFormState } from "react-dom"; + +import { + Button, + Grid, + GridContainer, + Label, + TextInput, +} from "@trussworks/react-uswds"; + +import { updateServerData } from "../../serverActions/serverActionExample"; + +const initialFormState = { + name: "", + email: "", +}; + +export default function SimpleForm() { + const [formData, updateFormData] = useFormState( + updateServerData, + initialFormState + ); + + const hasReturnedFormData = formData.name || formData.email; + + return ( + + + +
+
+

Server Action Example

+
+
+
+ + + + + + + + +
+ + {hasReturnedFormData && ( + <> +
+
+

Server Action returned data

+
+ {formData.name && ( +
+ Name: {formData.name} +
+ )} + {formData.email && ( +
+ Email: {formData.email} +
+ )} +
+
+ + )} +
+
+
+
+ ); +} diff --git a/app/src/app/serverActions/serverActionExample.ts b/app/src/app/serverActions/serverActionExample.ts new file mode 100644 index 00000000..6f671599 --- /dev/null +++ b/app/src/app/serverActions/serverActionExample.ts @@ -0,0 +1,30 @@ +"use server"; + +interface FormDataState { + name: string; + email: string; +} + +export async function updateServerData( + prevState: FormDataState, + formData: FormData +): Promise { + + + console.log("prevState => ", prevState); + console.log("formData => ", formData); + + const name = formData.get("name") as string; + const email = formData.get("email") as string; + + // In a real application, you would typically perform + // some server mutation. + await Promise.resolve(); + + const updatedData: FormDataState = { + name: name || prevState.name, + email: email || prevState.email, + }; + + return updatedData; +} From ed73953f91da5ee13d57ec1e51281b9da4a9423b Mon Sep 17 00:00:00 2001 From: Ryan Lewis Date: Wed, 29 May 2024 17:04:44 -0700 Subject: [PATCH 2/9] format --- app/src/app/serverActions/serverActionExample.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/src/app/serverActions/serverActionExample.ts b/app/src/app/serverActions/serverActionExample.ts index 6f671599..6d12f1f7 100644 --- a/app/src/app/serverActions/serverActionExample.ts +++ b/app/src/app/serverActions/serverActionExample.ts @@ -9,8 +9,6 @@ export async function updateServerData( prevState: FormDataState, formData: FormData ): Promise { - - console.log("prevState => ", prevState); console.log("formData => ", formData); From 9ce15981fe9deea83fa22f601404c98c0e4e286d Mon Sep 17 00:00:00 2001 From: Ryan Lewis <93001277+rylew1@users.noreply.github.com> Date: Thu, 30 May 2024 14:20:25 -0700 Subject: [PATCH 3/9] Update app/src/app/[locale]/serverAction/page.tsx Co-authored-by: Sawyer Hollenshead --- app/src/app/[locale]/serverAction/page.tsx | 87 +++++++++------------- 1 file changed, 36 insertions(+), 51 deletions(-) diff --git a/app/src/app/[locale]/serverAction/page.tsx b/app/src/app/[locale]/serverAction/page.tsx index 80031123..2b1a8a11 100644 --- a/app/src/app/[locale]/serverAction/page.tsx +++ b/app/src/app/[locale]/serverAction/page.tsx @@ -26,59 +26,44 @@ export default function SimpleForm() { const hasReturnedFormData = formData.name || formData.email; return ( - - - -
-
-

Server Action Example

-
-
-
- - + <> +

Server Action Example

- - + + + - - -
+ + + + + - {hasReturnedFormData && ( - <> -
-
-

Server Action returned data

-
- {formData.name && ( -
- Name: {formData.name} -
- )} - {formData.email && ( -
- Email: {formData.email} -
- )} -
-
- - )} -
-
-
-
+ {hasReturnedFormData && ( +
+

Server Action returned data

+ {formData.name && ( +
+ Name: {formData.name} +
+ )} + {formData.email && ( +
+ Email: {formData.email} +
+ )} +
+ )} + ); } From ab81b1072b82a1963173184887669eae9fead888 Mon Sep 17 00:00:00 2001 From: Ryan Lewis Date: Thu, 30 May 2024 15:27:35 -0700 Subject: [PATCH 4/9] add simulated delay on server action call --- app/src/app/serverActions/serverActionExample.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/app/serverActions/serverActionExample.ts b/app/src/app/serverActions/serverActionExample.ts index 6d12f1f7..2b7ba1ae 100644 --- a/app/src/app/serverActions/serverActionExample.ts +++ b/app/src/app/serverActions/serverActionExample.ts @@ -17,7 +17,7 @@ export async function updateServerData( // In a real application, you would typically perform // some server mutation. - await Promise.resolve(); + await new Promise((resolve) => setTimeout(resolve, 2000)); const updatedData: FormDataState = { name: name || prevState.name, From 46146f11c65e211a2c1b98aae13ed55f7d9c59b3 Mon Sep 17 00:00:00 2001 From: Ryan Lewis Date: Thu, 30 May 2024 15:28:50 -0700 Subject: [PATCH 5/9] add pending status button --- .../serverAction/PendingStatusSubmitButton.tsx | 16 ++++++++++++++++ app/src/app/[locale]/serverAction/page.tsx | 12 +++--------- 2 files changed, 19 insertions(+), 9 deletions(-) create mode 100644 app/src/app/[locale]/serverAction/PendingStatusSubmitButton.tsx diff --git a/app/src/app/[locale]/serverAction/PendingStatusSubmitButton.tsx b/app/src/app/[locale]/serverAction/PendingStatusSubmitButton.tsx new file mode 100644 index 00000000..601341bc --- /dev/null +++ b/app/src/app/[locale]/serverAction/PendingStatusSubmitButton.tsx @@ -0,0 +1,16 @@ +"use client"; + +import { Button } from "@trussworks/react-uswds"; +import { useFormStatus } from "react-dom"; + +function PendingStatusSubmitButton() { + const { pending } = useFormStatus(); + + return ( + + ); +} + +export default PendingStatusSubmitButton; diff --git a/app/src/app/[locale]/serverAction/page.tsx b/app/src/app/[locale]/serverAction/page.tsx index 2b1a8a11..64ced701 100644 --- a/app/src/app/[locale]/serverAction/page.tsx +++ b/app/src/app/[locale]/serverAction/page.tsx @@ -2,15 +2,10 @@ import { useFormState } from "react-dom"; -import { - Button, - Grid, - GridContainer, - Label, - TextInput, -} from "@trussworks/react-uswds"; +import { Label, TextInput } from "@trussworks/react-uswds"; import { updateServerData } from "../../serverActions/serverActionExample"; +import PendingStatusSubmitButton from "./PendingStatusSubmitButton"; const initialFormState = { name: "", @@ -24,7 +19,6 @@ export default function SimpleForm() { ); const hasReturnedFormData = formData.name || formData.email; - return ( <>

Server Action Example

@@ -46,7 +40,7 @@ export default function SimpleForm() { defaultValue={formData.email} /> - + {hasReturnedFormData && ( From 6b6f021581e3f1aa0a487eed84b316ed2ec2c763 Mon Sep 17 00:00:00 2001 From: Ryan Lewis Date: Thu, 30 May 2024 15:39:01 -0700 Subject: [PATCH 6/9] rename to SubmitButton and move to components folder --- .../{PendingStatusSubmitButton.tsx => SubmitButton.tsx} | 7 ++++--- app/src/app/[locale]/serverAction/page.tsx | 5 ++--- 2 files changed, 6 insertions(+), 6 deletions(-) rename app/src/app/[locale]/serverAction/{PendingStatusSubmitButton.tsx => SubmitButton.tsx} (77%) diff --git a/app/src/app/[locale]/serverAction/PendingStatusSubmitButton.tsx b/app/src/app/[locale]/serverAction/SubmitButton.tsx similarity index 77% rename from app/src/app/[locale]/serverAction/PendingStatusSubmitButton.tsx rename to app/src/app/[locale]/serverAction/SubmitButton.tsx index 601341bc..8fedd699 100644 --- a/app/src/app/[locale]/serverAction/PendingStatusSubmitButton.tsx +++ b/app/src/app/[locale]/serverAction/SubmitButton.tsx @@ -1,9 +1,10 @@ "use client"; -import { Button } from "@trussworks/react-uswds"; import { useFormStatus } from "react-dom"; -function PendingStatusSubmitButton() { +import { Button } from "@trussworks/react-uswds"; + +function SubmitButton() { const { pending } = useFormStatus(); return ( @@ -13,4 +14,4 @@ function PendingStatusSubmitButton() { ); } -export default PendingStatusSubmitButton; +export default SubmitButton; diff --git a/app/src/app/[locale]/serverAction/page.tsx b/app/src/app/[locale]/serverAction/page.tsx index 64ced701..ca9645ba 100644 --- a/app/src/app/[locale]/serverAction/page.tsx +++ b/app/src/app/[locale]/serverAction/page.tsx @@ -1,11 +1,10 @@ "use client"; -import { useFormState } from "react-dom"; - import { Label, TextInput } from "@trussworks/react-uswds"; +import PendingStatusSubmitButton from "./SubmitButton"; import { updateServerData } from "../../serverActions/serverActionExample"; -import PendingStatusSubmitButton from "./PendingStatusSubmitButton"; +import { useFormState } from "react-dom"; const initialFormState = { name: "", From a7c722de88abd38bd5c172babe054a6a2436577f Mon Sep 17 00:00:00 2001 From: Ryan Lewis Date: Thu, 30 May 2024 15:53:42 -0700 Subject: [PATCH 7/9] move action to actions.ts and components to components folder --- .../serverAction/actions.ts} | 0 app/src/app/[locale]/serverAction/page.tsx | 23 +++++++------ app/src/components/FormInput.tsx | 34 +++++++++++++++++++ .../SubmitButton.tsx | 0 4 files changed, 46 insertions(+), 11 deletions(-) rename app/src/app/{serverActions/serverActionExample.ts => [locale]/serverAction/actions.ts} (100%) create mode 100644 app/src/components/FormInput.tsx rename app/src/{app/[locale]/serverAction => components}/SubmitButton.tsx (100%) diff --git a/app/src/app/serverActions/serverActionExample.ts b/app/src/app/[locale]/serverAction/actions.ts similarity index 100% rename from app/src/app/serverActions/serverActionExample.ts rename to app/src/app/[locale]/serverAction/actions.ts diff --git a/app/src/app/[locale]/serverAction/page.tsx b/app/src/app/[locale]/serverAction/page.tsx index ca9645ba..e4b658ef 100644 --- a/app/src/app/[locale]/serverAction/page.tsx +++ b/app/src/app/[locale]/serverAction/page.tsx @@ -1,9 +1,11 @@ -"use client"; +// Server Action example +// For more context: https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations -import { Label, TextInput } from "@trussworks/react-uswds"; +"use client"; -import PendingStatusSubmitButton from "./SubmitButton"; -import { updateServerData } from "../../serverActions/serverActionExample"; +import FormInput from "../../../components/FormInput"; +import SubmitButton from "../../../components/SubmitButton"; +import { updateServerData } from "./actions"; import { useFormState } from "react-dom"; const initialFormState = { @@ -18,28 +20,27 @@ export default function SimpleForm() { ); const hasReturnedFormData = formData.name || formData.email; + return ( <>

Server Action Example

- - - - - - - + {hasReturnedFormData && ( diff --git a/app/src/components/FormInput.tsx b/app/src/components/FormInput.tsx new file mode 100644 index 00000000..58d73c38 --- /dev/null +++ b/app/src/components/FormInput.tsx @@ -0,0 +1,34 @@ +"use client"; + +import { Label, TextInput } from "@trussworks/react-uswds"; + +interface FormInputProps { + id: string; + name: string; + type: "number" | "search" | "text" | "email" | "password" | "tel" | "url"; + label: string; + defaultValue?: string; +} + +const FormInput: React.FC = ({ + id, + name, + type, + label, + defaultValue, +}) => { + return ( + <> + + + + ); +}; + +export default FormInput; diff --git a/app/src/app/[locale]/serverAction/SubmitButton.tsx b/app/src/components/SubmitButton.tsx similarity index 100% rename from app/src/app/[locale]/serverAction/SubmitButton.tsx rename to app/src/components/SubmitButton.tsx From 3b60b3c936b6fdfdad3bdca0149e4e2b2d8bc1e5 Mon Sep 17 00:00:00 2001 From: Ryan Lewis Date: Thu, 30 May 2024 18:16:20 -0700 Subject: [PATCH 8/9] move to clientForm component with translations --- app/src/app/[locale]/serverAction/actions.ts | 5 +- .../app/[locale]/serverAction/clientForm.tsx | 65 ++++++++++++++ app/src/app/[locale]/serverAction/page.tsx | 85 ++++++++----------- app/src/components/SubmitButton.tsx | 4 +- app/src/i18n/messages/en-US/index.ts | 9 ++ 5 files changed, 115 insertions(+), 53 deletions(-) create mode 100644 app/src/app/[locale]/serverAction/clientForm.tsx diff --git a/app/src/app/[locale]/serverAction/actions.ts b/app/src/app/[locale]/serverAction/actions.ts index 2b7ba1ae..b5bd235a 100644 --- a/app/src/app/[locale]/serverAction/actions.ts +++ b/app/src/app/[locale]/serverAction/actions.ts @@ -12,8 +12,9 @@ export async function updateServerData( console.log("prevState => ", prevState); console.log("formData => ", formData); - const name = formData.get("name") as string; - const email = formData.get("email") as string; + // With a server form, formData is null and only prevState can be used + const name = (formData?.get("name") as string) || prevState.name; + const email = (formData?.get("email") as string) || prevState.email; // In a real application, you would typically perform // some server mutation. diff --git a/app/src/app/[locale]/serverAction/clientForm.tsx b/app/src/app/[locale]/serverAction/clientForm.tsx new file mode 100644 index 00000000..2cb27b99 --- /dev/null +++ b/app/src/app/[locale]/serverAction/clientForm.tsx @@ -0,0 +1,65 @@ +"use client"; + +import { useFormState } from "react-dom"; + +import { useTranslations } from "next-intl"; +import { Label, TextInput } from "@trussworks/react-uswds"; + +import SubmitButton from "../../../components/SubmitButton"; +import { updateServerData } from "./actions"; + +const initialFormState = { + name: "", + email: "", +}; + +export default function ClientForm() { + const t = useTranslations("serverAction"); + const [formData, updateFormData] = useFormState( + updateServerData, + initialFormState + ); + + const hasReturnedFormData = formData.name || formData.email; + + return ( + <> +
+ + + + + + + + {hasReturnedFormData && ( +
+

{t("returnedDataHeader")}

+ {formData.name && ( +
+ {t("nameLabel")}: {formData.name} +
+ )} + {formData.email && ( +
+ {t("emailLabel")}: {formData.email} +
+ )} +
+ )} + + ); +} diff --git a/app/src/app/[locale]/serverAction/page.tsx b/app/src/app/[locale]/serverAction/page.tsx index e4b658ef..8a86f59e 100644 --- a/app/src/app/[locale]/serverAction/page.tsx +++ b/app/src/app/[locale]/serverAction/page.tsx @@ -1,63 +1,48 @@ // Server Action example // For more context: https://nextjs.org/docs/app/building-your-application/data-fetching/server-actions-and-mutations -"use client"; +import { pick } from "lodash"; +import { Metadata } from "next"; -import FormInput from "../../../components/FormInput"; -import SubmitButton from "../../../components/SubmitButton"; -import { updateServerData } from "./actions"; -import { useFormState } from "react-dom"; +import { + NextIntlClientProvider, + useMessages, + useTranslations, +} from "next-intl"; +import { getTranslations } from "next-intl/server"; -const initialFormState = { - name: "", - email: "", -}; +import ClientForm from "./clientForm"; -export default function SimpleForm() { - const [formData, updateFormData] = useFormState( - updateServerData, - initialFormState - ); +interface RouteParams { + locale: string; +} + +export async function generateMetadata({ params }: { params: RouteParams }) { + const t = await getTranslations({ locale: params.locale }); + const meta: Metadata = { + title: t("serverAction.title"), + }; + + return meta; +} + +type Props = { + locale?: string; +}; - const hasReturnedFormData = formData.name || formData.email; +export function SimpleForm({ locale }: Props) { + const messages = useMessages(); + const t = useTranslations("serverAction"); return ( <> -

Server Action Example

- -
- - - - - - {hasReturnedFormData && ( -
-

Server Action returned data

- {formData.name && ( -
- Name: {formData.name} -
- )} - {formData.email && ( -
- Email: {formData.email} -
- )} -
- )} + +

{t("title")}

+ +
); } diff --git a/app/src/components/SubmitButton.tsx b/app/src/components/SubmitButton.tsx index 8fedd699..f10f11c6 100644 --- a/app/src/components/SubmitButton.tsx +++ b/app/src/components/SubmitButton.tsx @@ -2,14 +2,16 @@ import { useFormStatus } from "react-dom"; +import { useTranslations } from "next-intl"; import { Button } from "@trussworks/react-uswds"; function SubmitButton() { + const t = useTranslations("serverAction"); const { pending } = useFormStatus(); return ( ); } diff --git a/app/src/i18n/messages/en-US/index.ts b/app/src/i18n/messages/en-US/index.ts index 5e856e07..0993602d 100644 --- a/app/src/i18n/messages/en-US/index.ts +++ b/app/src/i18n/messages/en-US/index.ts @@ -26,4 +26,13 @@ export const messages = { formatting: "The template includes an internationalization library with basic formatters built-in. Such as numbers: { amount, number, currency }, and dates: { isoDate, date, long}.", }, + serverAction: { + title: "Server Actions Example", + submitting: "Submitting...", + submit: "Submit", + nameLabel: "Name", + emailLabel: "Email", + submitLabel: "Submit", + returnedDataHeader: "Server Action returned data", + }, }; From 7eeaf44589f4cfeda83c5145667950e7e71af661 Mon Sep 17 00:00:00 2001 From: Ryan Lewis Date: Thu, 30 May 2024 18:25:57 -0700 Subject: [PATCH 9/9] update props to fix type errors --- app/src/app/[locale]/serverAction/page.tsx | 25 +++++++++++----------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/app/src/app/[locale]/serverAction/page.tsx b/app/src/app/[locale]/serverAction/page.tsx index 8a86f59e..e71d362e 100644 --- a/app/src/app/[locale]/serverAction/page.tsx +++ b/app/src/app/[locale]/serverAction/page.tsx @@ -26,23 +26,22 @@ export async function generateMetadata({ params }: { params: RouteParams }) { return meta; } -type Props = { - locale?: string; -}; +interface Props { + params: RouteParams; +} -export function SimpleForm({ locale }: Props) { +export default function SimpleForm({ params }: Props) { + const { locale } = params; const messages = useMessages(); const t = useTranslations("serverAction"); return ( - <> - -

{t("title")}

- -
- + +

{t("title")}

+ +
); }