DEV Community

Cover image for Use Server Side Forms in NextJS
Jawad Ul Hassan
Jawad Ul Hassan

Posted on

Use Server Side Forms in NextJS

I utilized Server Actions, React Server Components (RSC), and Next’s App Router while exploring Forms in Next.js 14. The intrinsic aspects of React/Next like useFormStatus, useFormState, and revalidatePath are covered in this extensive blog. Form validation, with its detailed insight, is covered in this blog by using Zod, error handling on a fine- and coarse-grainy level, utilizing the web's progressive enhancement capabilities to provide user feedback through field-level errors and toast notifications, while remaining functional without the need of JavaScript in the browser.

I started with the first section of the App which is the page, then I worked on the following sections: Actions, directory-create-form submit-button, and toast-provider then I moved towards the last section of the app which is the message list.

For the sake of simplicity, let's just add more Tailwind CSS and use it for the loading barrier. Let's stay focused since adding form validation and form feedback will complicate it.

Operating Forms in NextJS

I began by installing the most recent version of Next.js via Next's App Router. To supply them all, a message list component is created using Server Actions and React Server Components (RSC), and a form component named directory-creates-form is created on the root page.

page.tsx (src/app/page)

import { DirectoryCreateForm } from "@/components/directory-create-form";
import { MessageList } from "@/components/message-list";


export default function Home() {
 return (
   <>
     {/* Background SVG */}
     <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1440 320">
       <path
         fill="rgb(59 130 246)"
         fillOpacity="1"
         d="M0,160L80,170.7C160,181,320,203,480,186.7C640,171,800,117,960,106.7C1120,96,1280,128,1360,144L1440,160L1440,0L1360,0C1280,0,1120,0,960,0C800,0,640,0,480,0C320,0,160,0,80,0L0,0Z"
       ></path>
     </svg>


     {/* Main content */}
     <main className="w-full flex flex-row p-8">
       {/* Directory Create Form */}
       <div className="w-1/2">
         <DirectoryCreateForm />
       </div>


       {/* Divider */}
       <div className="border-l-2 border-blue mx-6 h-96"></div>


       {/* Message List */}
       <div className="w-1/2">
         <MessageList />
       </div>
     </main>
   </>
 );
}

Enter fullscreen mode Exit fullscreen mode

Operating actions

I implemented the getMessages function in the action.ts file while the createMessage function is yet absent but is used in the action attribute of the form. So createMessage function will be created there so that both these functions can be operated on the same artificial data. But that is a server action at that point.

First arguments are taken from the FormData object, which holds the entire data of the form, through the movement of Server actions which are sent to the form action attribute. I utilized this object to add new messages to the messages array as the value of the text field which are four inputs.

For the simulation of a slow network, I added a pseudo delay of 300ms. Furthermore, the messages array was mutated in memory, to add a new message to it.

I also used Next's revalidatePath function to revalidate the page's route ('/') to see new messages in the list even after the submission of the form (if any) without reloading the page.

action.tsx (src/app/action)


"use server";


import { z } from "zod";
import { revalidatePath } from "next/cache";
import {
 FormState,
 fromErrorToFormState,
 toFormState,
} from "@/utils/to-form-state";


type Message = {
 id: string;
 name: string;
 employeeId: string;
 contactNo: string;
 dateOfJoining: Date;
};


let messages: Message[] = [];


export const getMessages = async (): Promise<Message[]> => {
 await new Promise((resolve) => setTimeout(resolve, 300));


 return Promise.resolve(messages);
};


// Regular expression for validating phone numbers
const phoneNumberRegex = /^\d{10,15}$/;


// Regular expression for validating employee ID
const empIdRegex = /^\d{3,5}$/;


const createMessageSchema = z.object({
 name: z.string().min(1).max(50),
 employeeId: z.string().refine((value) => empIdRegex.test(value), {
   message: "Invalid Employee ID. Must be 3 to 5 digits.",
 }),
 contactNo: z.string().refine((value) => phoneNumberRegex.test(value), {
   message: "Invalid Phone number. Must be 10 to 15 digits.",
 }),
 dateOfJoining: z.string().transform((val, ctx) => {
   const date = new Date(val);
   if (isNaN(date.getTime())) {
     ctx.addIssue({
       code: z.ZodIssueCode.custom,
       message: "Invalid date. Must be a valid date string.",
     });
     return z.NEVER;
   }
   return date;
 }),
});


export const createMessage = async (
 formState: FormState,
 formData: FormData
) => {
 await new Promise((resolve) => setTimeout(resolve, 300));


 try {
   const data = createMessageSchema.parse({
     name: formData.get("name"),
     employeeId: formData.get("employeeId"),
     contactNo: formData.get("contactNo"),
     dateOfJoining: formData.get("dateOfJoining"),
   });


   messages.push({
     id: crypto.randomUUID(),
     ...data,
   });


   revalidatePath("/");


   return toFormState("SUCCESS", "Employee successfully created");
 } catch (error) {
   return fromErrorToFormState(error);
 }
};

Enter fullscreen mode Exit fullscreen mode

Operating Directory Create Form State

React's useFormState Hook is used with a server action. It enables the transfer of customer data in the middle, between the client component and the server action. Arguably, the new hook should consist of an initial state object and the server action. Together with the current form state, the formState and the improved server action are returned.

directory-create-form.tsx (src/compnents/directory-create-form)

"use client";


import { useFormState } from "react-dom";
import { createMessage } from "@/app/actions";
import { SubmitButton } from "./submit-button";
import { EMPTY_FORM_STATE } from "@/utils/to-form-state";
import { useToastMessage } from "@/hooks/use-toast-message";
import { FieldError } from "./field-error";
import { useFormReset } from "@/hooks/use-form-reset";


const DirectoryCreateForm = () => {
 const [formState, action] = useFormState(createMessage, EMPTY_FORM_STATE);
 const noScriptFallback = useToastMessage(formState);
 const formRef = useFormReset(formState);


 return (
   <form action={action} ref={formRef} className="flex flex-col gap-y-2">
     <FormField
       label="Name"
       name="name"
       placeholder="Please Enter Name"
       formState={formState}
     />
     <FormField
       label="Employee ID"
       name="employeeId"
       placeholder="Please Enter Employee ID"
       formState={formState}
     />
     <FormField
       label="Contact No"
       name="contactNo"
       placeholder="Please Enter Contact No"
       formState={formState}
     />
     <FormField
       label="Date of Joining"
       name="dateOfJoining"
       placeholder="Please Enter Date of Joining"
       formState={formState}
     />


     <SubmitButton label="Create" loading="Creating ..." />


     {noScriptFallback}
   </form>
 );
};


type FormFieldProps = {
 label: string;
 name: string;
 placeholder: string;
 formState: any; // Adjust the type according to your form state type
};


const FormField: React.FC<FormFieldProps> = ({
 label,
 name,
 placeholder,
 formState,
}) => (
 <>
   <label htmlFor={name}>{label}</label>
   <input
     id={name}
     name={name}
     className="border-2 rounded p-2"
     placeholder={placeholder}
   />
   <FieldError formState={formState} name={name} />
 </>
);


export { DirectoryCreateForm };
Enter fullscreen mode Exit fullscreen mode

Operating Message List

At this stage, the server component that handles data delivery on the server makes its list of messages available. The server component uses await to fetch data, while the function component is designated async. This must be kept in mind.

message-list.tsx (src/components/message-list)

import { getMessages } from "@/app/actions";


const formatDate = (date: Date): string => {
 return date.toISOString().split("T")[0]; // Converts date to 'YYYY-MM-DD' format
};


const MessageList: React.FC = async () => {
 const directories = await getMessages();


 return (
   <>
     <h3 className="font-sans text-lg">Employees Directories</h3>
     <hr className="my-3" />
     <ul className="overflow-y-auto h-80">
       {directories.map((directory) => (
         <div key={directory.id}>
           <ListItem label="Name" value={directory.name} />
           <ListItem label="Employee ID" value={directory.employeeId} />
           <ListItem label="Contact Number" value={directory.contactNo} />
           <ListItem
             label="Date Of Joining"
             value={formatDate(directory.dateOfJoining)}
           />
           <hr className="my-2" />
         </div>
       ))}
     </ul>
   </>
 );
};


type ListItemProps = {
 label: string;
 value: string | Date;
};


const ListItem: React.FC<ListItemProps> = ({ label, value }) => (
 <li className="p-1">
   {label}: {typeof value === "string" ? value : formatDate(value)}
 </li>
);


export { MessageList };

Enter fullscreen mode Exit fullscreen mode

Conclusion

I hope you enjoyed this lengthy blog post as I wrap it off. This blog covers forms in Next.Js that have server actions and server components. It displays both sides of this new paradigm:
It is demonstrated that a web-native world with progressive enhancement may work without the requirement for JavaScript. Still, a great deal of effort had to be made to attain a result. The fundamental building pieces that are being used are provided by Next and React. A lot of things are provided by default by the framework. Imagine if a new useAction hook could be created using a framework consisting of useFormReset, useToastMessage, and useFormState. With that, all of the manual tasks completed in this blog would be resolved.

Here is a full code sandbox code for example.
https://codesandbox.io/p/github/jawadulhassan/forms-server-actions/main

Nextjs Forms and Mutation.
https://nextjs.org/docs/pages/building-your-application/data-fetching/forms-and-mutations

Top comments (0)