Skip to main content

Form Action

Key Concepts in Form Handling with React and Next.js

Server Components and Server Actions

Server Components in React allow functions to execute on the server, reducing the client-side load. Next.js leverages this feature through Server Actions, which connect forms directly to server-side functions. This eliminates the need for client-side state management typically associated with forms, like using useState.

Simplified Data Handling

Instead of manually managing each form field's state, Next.js enables the automatic handling of form data through Server Actions. When a form is submitted, the associated Server Action receives a FormData object, which can be directly manipulated using standard methods to extract or manipulate data.

Enhancements for Complex Forms

For forms with multiple fields, Next.js simplifies data extraction and handling. Developers can efficiently manage and validate complex data sets, which is particularly useful in enterprise-level applications where forms are data-intensive.

Advanced State Management with React Hooks

Next.js supports React's hooks for managing form states, such as:

  • useFormStatus to detect when a form is in a pending state, enabling the UI to respond (e.g., disabling a submit button while data is being processed).
  • useOptimistic for optimistic UI updates, which provide immediate feedback by assuming the success of server actions without waiting for a server response.

Server-side Validation

Next.js forms can integrate advanced server-side validation mechanisms. By leveraging schemas or custom validation logic, developers can ensure data integrity and respond with appropriate error messages or corrections before data mutation occurs.

Passing Additional Parameters

Next.js offers flexibility in how additional data is passed to Server Actions. Techniques like binding additional arguments or using hidden form fields allow for dynamic and secure data transmission without exposing sensitive information in the client-side code.

Optimistic UI Updates

Optimistic updates are a crucial feature for improving user experience by making the interface feel more responsive. Next.js forms can predict the outcome of server actions, updating the UI ahead of the actual server response. This approach is valuable in scenarios like messaging systems or comment sections, where immediate feedback is critical.

Implementation

Auth Actions

  • for our actions, as far as being able to sign in and sign up,we're gonna make a file called auth.These are gonna be all the actions we use for authentication.So let's do that.
  • But literally on the route of the repo, I'm gonna make a new folder called actions.And inside of there, a new file called auth.ts.
  • using use server this page should be processed on the server. Use server should force it, right?Yeah, this use server is a hint to the React compiler that this file should be ran in a NodeJS environment, basically.You cannot use an action, a server action without that server action being scoped inside of a use server directive.
Zod

Zod we are gonna use this for schema validation so we can validate our inputs.It is a very popular schema validation tool.If you don not know what Zod is, it's just a runtime schema validation library.Like always, never trust a client.Probably wanna validate these inputs before you do anything.So I'm using Zod here to make a schema for that, so I just created a schema.

  • Redirect, so we can redirect to the dashboard once someone signs in or signs up, and the name of the cookie that we'll be setting.
registerUser function
  • First argument is gonna be the previous state of the form that this action is gonna be bound to.
  • But we're gonna be using this special hook that basically provides the previous state of the form as the first argument into which the action that you give it is part of this special hook that we're gonna be using.
  • And then the second argument is actually gonna be the form data,which is gonna be type formData.If you don't know what formData type is, it's just a map.It's a map of all the input fields by the name that you gave them in the form.
cookies

The reason I'm setting them in the cookie is because I want to be able to access the JSON Web Token on all of my server-side calls and functions which have access to the cookies.If I store the JSON Web Token in a local storage or something like that,I would not be able to access that JSON Web Token server-side.There's no way to access local storage from a server.

/actions/auth.ts
"use server";
import { cookies } from "next/headers";
import { signin, signup } from "@/utils/authTools";
import { z } from "zod";
import { redirect } from "next/navigation";
import { COOKIE_NAME } from "@/utils/constants";

const authSchema = z.object({
email: z.string().email(),
password: z.string(),
});

export const registerUser = async (prevState: any, formData: FormData) => {
const data = authSchema.parse({
email: formData.get("email"),
password: formData.get("password"),
});

// try/catch block to handle errors
try {
const { token } = await signup(data);
cookies().set(COOKIE_NAME, token);
} catch (e) {
console.error(e);
return { message: "Failed to sign you up" };
}
//redirect to /dashboard.
// You might be asking why I don't just put the redirect here and the try after here.
// There is a bug for some reason on this redirect.I had to go deep into the Next.js issues to figure this out.
redirect("/dashboard");
};

export const signinUser = async (prevState: any, formData: FormData) => {
const data = authSchema.parse({
email: formData.get("email"),
password: formData.get("password"),
});

try {
const { token } = await signin(data);
cookies().set(COOKIE_NAME, token);
} catch (e) {
console.error(e);
return { message: "Failed to sign you in" };
}
redirect("/dashboard");
};

Submit Button

/components/Submit.tsx
"use client";

import { Button } from "@nextui-org/react";
import { useFormStatus } from "react-dom";

const Submit = ({ label, ...btnProps }) => {
const { pending } = useFormStatus();
return (
<Button {...btnProps} type="submit" isLoading={pending}>
{label}
</Button>
);
};

export default Submit;

Signup Form

useFormState

So first what we want is to useFormState.

  • Notice, that comes from react-dom.
  • That does not come from Next.js.That is a React thing.So we got that.
  • We're gonna create an initial state.Since we're only passing a message,we're just gonna have our initial state just be a null message like this.I'm gonna create our hook, useFormState.
  • useFormState takes in two arguments.It's gonna take in the actual action you want to run when the form is submitted as the first argument, and then the initial state as the second argument.
info
  • If you replicate this form as a server component,then you couldn't handle any of the interactions,because interactions are only possible on the client.So as soon as you go server component, you have no interactions.
  • So server components are typically read-only.
  • Yeah, so a forum, I would imagine if you added a forum,you want some type of interaction.So yeah, it wouldn't be a simple way to do that.
/components/SignupForm.tsx
"use client";

import { useFormState } from "react-dom";
import { Input, Button } from "@nextui-org/react";
import { registerUser } from "@/actions/auth";
import Link from "next/link";
import Submit from "./Submit";

const initState = { message: null };

const SignupForm = () => {
const [formState, action] = useFormState<{ message: string | null }>(
registerUser,
initState
);

return (
<form
action={action}
className="bg-content1 border border-default-100 shadow-lg rounded-md p-3 flex flex-col gap-2 "
>
<h3 className="my-4">Sign up</h3>
<Input fullWidth size="lg" placeholder="Email" name="email" required />
<Input
name="password"
fullWidth
size="lg"
type="password"
placeholder="Password"
required
/>
<Submit label={"signup"} />
<div>
<Link href="/signin">{`Already have an account?`}</Link>
</div>
{formState?.message && <p>{formState.message}</p>}
</form>
);
};

export default SignupForm;

Signin Form

/components/SigninForm.tsx
"use client";

import { useFormState } from "react-dom";
import { Input, Button } from "@nextui-org/react";
import { signinUser } from "@/actions/auth";
import Link from "next/link";
import Submit from "./Submit";

const initState = { message: null };

const SigninForm = () => {
const [formState, action] = useFormState<{ message: string | null }>(
signinUser,
initState
);

return (
<form
action={action}
className="bg-content1 border border-default-100 shadow-lg rounded-md p-3 flex flex-col gap-2 "
>
<h3 className="my-4">Sign in</h3>
<Input
fullWidth
required
size="lg"
placeholder="Email"
name="email"
type="email"
/>
<Input
name="password"
fullWidth
required
size="lg"
type="password"
placeholder="Password"
/>
<Submit label={"signin"} />
<div>
<Link href="/signup">{`Don't have an account?`}</Link>
</div>
{formState?.message && <p>{formState.message}</p>}
</form>
);
};

export default SigninForm;