Creating a Server Action for Form Data
So far, we've explored two out of four different types of interaction with the server: an API endpoint that takes JSON and another that takes form data.
Now we'll dive into creating a server action and using it to submit data.
Creating a Server Action
Server actions can be placed within a React server component, like a page, or within a separate file. For now, we'll create the server action inside src/app/page.tsx
.
The server action will be an asynchronous function named onDataAction
that we'll pass into the RegistrationForm
client component:
// inside page.tsx
export default function Home() {
const onDataAction = async (data: any) => {
console.log(data);
}
return (
<div className="mx-auto max-w-xl">
<RegistrationForm onDataAction={onDataAction} /> // red squiggly line under onDataAction
</div>
);
}
However, there's an error underneath the onDataAction
prop because it isn't defined as a property on RegistrationForm
.
On the page in the browser, we are shown an error:
Unhandled Runtime Error
Error: Event handlers cannot be passed to Client Component props.
<... onDataAction={function}>
^^^^^^^^^^
If you need interactivity, consider converting part of this to a Client Component.
Next.js thinks that onDataAction
is an event handler because it doesn't know that it's a server action.
In order to resolve this, we need to include "use server";
inside of the onDataAction
function to notify Next.js that the function is server-only:
const onDataAction = async (data: any) => {
"use server";
console.log(data);
}
With this change, the error goes away in the browser but TypeScript isn't happy that onDataAction
not being defined as a property on RegistrationForm
.
Let's fix it.
Adding onDataAction
to RegistrationForm
Over in RegistrationForm.tsx
, we'll define onDataAction
as a function that takes data
and returns a promise. For now we'll broadly define the data
type as any
, and the return type will be Promise<void>
since nothing is returned:
// inside RegistrationForm.tsx
export const RegistrationForm = ({
onDataAction,
}: {
onDataAction: (data: any) => Promise<void>;
}) => {
// ...
}
With the onDataAction
function defined and wired up, we can test it out.
Testing the Server Action
On the web page, fill out the form and hit submit.
Nothing seems to happen in the browser console where we previously saw our console.log()
output.
Instead, we'll check the terminal and see that our data has successfully been sent to the server:
Now that we know our submit is working properly, let's add the proper types to our server action.
Adding Typing and Validating Data
Back in page.tsx
we import z
from Zod and our schema
:
import { z } from "zod";
import { schema } from "@app/registrationSchema";
Inside the onDataAction
function, we'll type data
using the z.infer
trick we've used before. We also will use the schema's safeParse
to parse the data and use the conditional logic to check if the parsing was successful:
const onDataAction = async (data: z.infer<typeof schema>) => {
"use server";
const parsed = schema.safeParse(data);
if (parsed.success) {
console.log("User registered");
return { message: "User registered", user: parsed.data };
} else {
return {
message: "Invalid data",
issues: parsed.error.issues.map((issue) => issue.message),
}
}
}
Now that we've made these changes, TypeScript gives us another error on the onDataAction
prop in page.tsx
because the output of the function isn't void
anymore. It's now an object that contains either a success message and user data, or an error message and the issues.
In RegistrationForm.tsx
, we need to modify the promise to match the new schema.
The Promise will now return an object that will contain a message
, and might contain a user
or issues
. The user
will be inferred from the schema
, and issues
will be a string array:
export const RegistrationForm = ({
onDataAction,
}: {
onDataAction: (data: z.infer<typeof schema>) => Promise<{
message: string;
user?: z.infer<typeof schema>;
issues?: string[];
}>;
}) => {
...
We'll also update the onSubmit
function to log out the output. Remember to use await
since we're dealing with a promise:
const onSubmit = async (data: z.infer<typeof schema>) => {
// commented out fetch implementations above
console.log(await onDataAction(data));
}
Testing our work in the browser, we can submit the form and see the output in the console as expected.
Wrapping Up & Next Steps
We've now completed the third technique for form data handling, this time using a Server Action.
For our fourth method, we will reuse form data again by rewriting the existing code to use it.
Follow the included resources below to try it yourself, then check back in the next video to see how I did it.
Resources for Posting Form Data to a Server Action
In order to post form data to the server action, you'll be able to reuse much of the code from the onDataAction
to create a new onFormAction
function.
The new onFormAction
function will accept FormData
which will be parsed after being turned into a simple object with Object.fromEntries
.
Then over on the client side, you'll need to update onSubmit
function to create a formData
object based on the submitted data, then call formAction
with the formData
as an argument.
As an alternative, you could use the useFormState
hook and React refs to manage the form state and submit the form data.
We'll look at both solutions in the next video.