Part 106: Enhancing Form Validation in Next.js: Ensuring Robust User Input

[App] Server Actions

[App] Server Actions

In our journey to build a fully functional comment form with Next.js, we've reached a pivotal point. While the form successfully posts comments and regenerates the ReviewPage for all users, it currently lacks validation. As it stands, users can submit empty forms, resulting in blank comments displayed on the page. Let's explore how we can address this issue by implementing both client-side and server-side validation, ensuring that our application remains robust and secure.

Adding Client-Side Validation

Client-side validation enhances user experience by providing immediate feedback. Modern browsers support standard HTML form validation attributes, such as required, minlength, and maxlength, which we can utilize to prevent users from submitting invalid data.

Here's how we can implement basic client-side validation:

  1. Require Fields: Add the required attribute to both the "user" and "message" fields. This prevents form submission if these fields are empty.

  2. Set Maximum Length: Specify a maximum length for input fields. For example, limit the "user" field to 50 characters and the "message" field to 500 characters.

With these changes, browsers will automatically prompt users to fill in required fields, enhancing the overall user experience.

Code Example

Here's how the form looks with added client-side validation:

// components/CommentForm.jsx

import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
import { createComment } from '@/lib/comments';

export default function CommentForm({ slug, title }) {
  async function action(formData) {
    'use server';
    if (!formData.get('user')) {
      return { isError: true, message: 'Name field is required' };
    }
    const comment = await createComment({
      slug,
      user: formData.get('user'),
      message: formData.get('message'),
    });

    console.log('created:', comment);
    revalidatePath(`/reviews/${slug}`);
    redirect(`/reviews/${slug}`);
  }

  return (
    <form action={action} className="border bg-white flex flex-col gap-2 mt-3 px-3 py-3 rounded">
      <p className="pb-1">
        Already played <strong>{title}</strong>? Have your say!
      </p>
      <div className="flex">
        <label htmlFor="userField" className="shrink-0 w-32">
          Your name
        </label>
        <input id="userField" name="user" required maxLength={50} className="border px-2 py-1 rounded w-48" />
      </div>
      <div className="flex">
        <label htmlFor="messageField" className="shrink-0 w-32">
          Your comment
        </label>
        <textarea id="messageField" name="message" required maxLength={500} className="border px-2 py-1 rounded w-full" />
      </div>
      <button type="submit" className="bg-orange-800 rounded px-2 py-1 self-center">Submit</button>
    </form>
  );
}

The Limitations of Client-Side Validation

While client-side validation is beneficial for user experience, it should not be relied upon for security. Users can easily bypass these checks, for instance, by setting the novalidate attribute on the form or by manipulating HTTP requests directly. Therefore, it's essential to implement server-side validation to ensure that only valid data is processed and stored.

Implementing Server-Side Validation

Server-side validation acts as a second line of defense, verifying data integrity before processing it. In our example, we'll check if the "user" field is filled out before proceeding with comment creation. If the validation fails, we'll return an error message.

Handling Server Action Responses

To manage the server action response, we need to incorporate client-side logic, which requires converting CommentForm into a client component. This change allows us to display error messages to users when validation fails.

Code Example

Here's how you can structure your server-side validation:

// components/CommentForm.jsx

// 'use client';

import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
import { createComment } from '@/lib/comments';

export default function CommentForm({ slug, title }) {
  async function action(formData) {
    'use server';
    if (!formData.get('user')) {
      return { isError: true, message: 'Name field is required' };
    }
    const comment = await createComment({
      slug,
      user: formData.get('user'),
      message: formData.get('message'),
    });
    console.log('created:', comment);
    revalidatePath(`/reviews/${slug}`);
    redirect(`/reviews/${slug}`);
  }

  return (
    <form action={action}
      className="border bg-white flex flex-col gap-2 mt-3 px-3 py-3 rounded">
      <p className="pb-1">
        Already played <strong>{title}</strong>? Have your say!
      </p>
      <div className="flex">
        <label htmlFor="userField" className="shrink-0 w-32">
          Your name
        </label>
        <input id="userField" name="user" required maxLength={50}
          className="border px-2 py-1 rounded w-48"
        />
      </div>
      <div className="flex">
        <label htmlFor="messageField" className="shrink-0 w-32">
          Your comment
        </label>
        <textarea id="messageField" name="message" required maxLength={500}
          className="border px-2 py-1 rounded w-full"
        />
      </div>
      <button type="submit"
        className="bg-orange-800 rounded px-2 py-1 self-center
                   text-slate-50 w-32 hover:bg-orange-700">
        Submit
      </button>
    </form>
  );
}

Introducing React Hook Form

For more advanced form handling and validation, consider using React Hook Form. This library simplifies form management in React applications, offering features like easy integration with UI libraries, better performance by reducing re-renders, and built-in validation support.

Benefits of React Hook Form

  • Performance: Minimizes re-renders, improving form performance.

  • Ease of Use: Simple API that integrates seamlessly with React components.

  • Validation: Offers built-in validation with support for custom rules.

  • Integration: Easily works with UI libraries to create complex forms.

Implementing React Hook Form

To get started with React Hook Form, you need to install it via npm:

npm install react-hook-form

Then, integrate it into your form component:

// components/CommentForm.jsx

import { useForm } from 'react-hook-form';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';
import { createComment } from '@/lib/comments';

export default function CommentForm({ slug, title }) {
  const { register, handleSubmit, formState: { errors } } = useForm();

  async function onSubmit(data) {
    'use server';
    if (!data.user) {
      return { isError: true, message: 'Name field is required' };
    }
    const comment = await createComment({
      slug,
      user: data.user,
      message: data.message,
    });

    console.log('created:', comment);
    revalidatePath(`/reviews/${slug}`);
    redirect(`/reviews/${slug}`);
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)} className="border bg-white flex flex-col gap-2 mt-3 px-3 py-3 rounded">
      <p className="pb-1">
        Already played <strong>{title}</strong>? Have your say!
      </p>
      <div className="flex">
        <label htmlFor="userField" className="shrink-0 w-32">
          Your name
        </label>
        <input
          id="userField"
          {...register('user', { required: true, maxLength: 50 })}
          className="border px-2 py-1 rounded w-48"
        />
        {errors.user && <span className="text-red-500">Name is required</span>}
      </div>
      <div className="flex">
        <label htmlFor="messageField" className="shrink-0 w-32">
          Your comment
        </label>
        <textarea
          id="messageField"
          {...register('message', { required: true, maxLength: 500 })}
          className="border px-2 py-1 rounded w-full"
        />
        {errors.message && <span className="text-red-500">Comment is required</span>}
      </div>
      <button type="submit" className="bg-orange-800 rounded px-2 py-1 self-center">Submit</button>
    </form>
  );
}

The Limitations of Client-Side Validation

While client-side validation is beneficial for user experience, it should not be relied upon for security. Users can easily bypass these checks, for instance, by setting the novalidate attribute on the form or by manipulating HTTP requests directly. Therefore, it's essential to implement server-side validation to ensure that only valid data is processed and stored.

Conclusion

By integrating both client-side and server-side validation, alongside leveraging a library like React Hook Form, you can create a more robust and secure application. Client-side validation enhances user experience with immediate feedback, while server-side validation ensures data integrity and security. As a best practice, always validate and sanitize user input on the server to protect your application from potential exploits and ensure reliable data processing.

Last updated