Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Issue #236]: Server action example #341

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
84 changes: 84 additions & 0 deletions app/src/app/[locale]/serverAction/page.tsx
rylew1 marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be helpful for the example to also demonstrate how to test the page, as well as render it in Storybook.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can give tests and storybook a shot when we decide on how we want to construct these example(s)

Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"use client";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are there alternative approaches where the entire page doesn't need opted out of being a server component?

Copy link
Contributor Author

@rylew1 rylew1 May 30, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No expert at this, but I believe you can have server only forms - but then you can't demo the client hooks like useFormStatus and useFormState (not that these hooks have to be used with a server action). For a server-based form, instead of useFormState you would just call the server action function directly in the form's action attribute - but you can't do any user input in textboxes and such.

I'm not sure if another server (second) example makes sense for comparison? IE - Just call a server action from a server component page - and pass the resulting data down into a client component that displays it.

Just wanted to get things started on this cause I think it would have helped on our project if we had these examples.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think one alternative is to have a separate component for the part that depends on the client-only hook. I think for most forms it's likely just the submit button, so it might be something like:

// components/SubmitButton.tsx
"use client";

function SubmitButton(props) {
  const { pending } = useFormStatus();

  return <button disabled={pending} type="submit">{props.label}</button>;
}

I think that would then allow the page to be a server component with a small client-side island.

Copy link
Contributor Author

@rylew1 rylew1 May 31, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@sawyerh this makes sense and we can try this - but we won't be able to demo useFormState without making the form (and text inputs) client components. I updated it to use a page but with a client form component with useFormState .

It can work with the form in a server component (with some eslint rules turned off (no-misused-promise). Lmk if we want multiple examples or something.


import { useFormState } from "react-dom";
rylew1 marked this conversation as resolved.
Show resolved Hide resolved

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 (
<GridContainer>
<Grid row>
<Grid col={12}>
<div className="usa-card">
<div className="usa-card__header">
<h2>Server Action Example</h2>
rylew1 marked this conversation as resolved.
Show resolved Hide resolved
</div>
<div className="usa-card__body">
<form action={updateFormData}>
<Label htmlFor="name">Name</Label>
<TextInput
id="name"
name="name"
type="text"
defaultValue={formData.name}
/>

<Label htmlFor="email">Email</Label>
<TextInput
id="email"
name="email"
type="email"
defaultValue={formData.email}
className="margin-bottom-1"
/>

<Button type="submit">Submit</Button>
rylew1 marked this conversation as resolved.
Show resolved Hide resolved
</form>
</div>

{hasReturnedFormData && (
<>
<hr />
<div className="usa-card margin-top-2">
<h4>Server Action returned data</h4>
<div className="usa-card__body">
{formData.name && (
<div>
<strong>Name:</strong> {formData.name}
</div>
)}
{formData.email && (
<div>
<strong>Email:</strong> {formData.email}
</div>
)}
</div>
</div>
</>
)}
</div>
</Grid>
</Grid>
</GridContainer>
rylew1 marked this conversation as resolved.
Show resolved Hide resolved
);
}
30 changes: 30 additions & 0 deletions app/src/app/serverActions/serverActionExample.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"use server";

interface FormDataState {
name: string;
email: string;
}

export async function updateServerData(
prevState: FormDataState,
formData: FormData
): Promise<FormDataState> {


console.log("prevState => ", prevState);
console.log("formData => ", formData);
rylew1 marked this conversation as resolved.
Show resolved Hide resolved

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();
rylew1 marked this conversation as resolved.
Show resolved Hide resolved

const updatedData: FormDataState = {
name: name || prevState.name,
email: email || prevState.email,
};

return updatedData;
}
Loading