Part 109: Handling Server-Side Validation and User Feedback in Next.js

[App] Server Actions

[App] Server Actions

In modern web development, balancing client-side and server-side functionality is crucial for creating responsive and secure applications. One such scenario is handling form submissions, where we need to validate user inputs both on the client and server sides. In this blog post, we'll explore how to manage server-side validation and display error messages to users using Next.js, while ensuring that successful submissions clear form fields.

Directly Calling Server Actions from Client-Side Code

In Next.js, server actions can be invoked directly from client-side code. This enables us to capture the response from the server and act accordingly. Let's break down how to handle server responses, particularly error messages, and display them to users.

Displaying Error Messages

When a form submission fails due to missing required fields, the server action returns an error object. Our goal is to capture this object and display a meaningful message to the user. Here's how we can achieve that:

  1. Check the Server Action Result: The result of the server action can be either an error object or undefined when successful. We use the optional chaining operator (?.) to safely check if the result is an error.

  2. Manage Error State: We use the useState hook to create a state variable error. This state will store the error object, if any, allowing us to conditionally render error messages.

  3. Reset Error on Resubmission: We reset the error state to null when the form is resubmitted to ensure that error messages are cleared when the user corrects their input.

  4. Clear Form on Success: If the form submission is successful, we reset the form fields to provide a clean slate for the user.

Implementing Client-Side Logic

Here's the code implementation that demonstrates these concepts:

// components/CommentForm.jsx

'use client';

import { useState } from 'react';
import { createCommentAction } from '@/app/reviews/[slug]/actions';

export default function CommentForm({ slug, title }) {
  const [error, setError] = useState(null);

  const handleSubmit = async (event) => {
    event.preventDefault();
    setError(null);
    const form = event.currentTarget;
    const formData = new FormData(form);
    const result = await createCommentAction(formData);
    if (result?.isError) {
      setError(result);
    } else {
      form.reset();
    }
  };

  return (
    <form onSubmit={handleSubmit}
      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>
      <input type="hidden" name="slug" value={slug} />
      <div className="flex">
        <label htmlFor="userField" className="shrink-0 w-32">
          Your name
        </label>
        <input id="userField" name="user"
          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"
          className="border px-2 py-1 rounded w-full"
        />
      </div>
      {Boolean(error) && (
        <p className="text-red-700">{error.message}</p>
      )}
      <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>
  );
}

Key Concepts:

  • useState Hook: We use this to manage the error state, which stores any error messages returned by the server.

  • handleSubmit Function: This function is triggered when the form is submitted. It prevents the default form submission, gathers the form data, calls the server action, and handles the result.

  • Error Handling: If the server action returns an error, we update the error state, which causes the error message to be displayed in the form.

  • Form Reset: On successful submission, the form fields are cleared using form.reset().

Validating Form Data on the Server

Server-side validation is crucial for ensuring data integrity and security. In our setup, we validate each field in the server action. If any field is invalid, we return an error message specific to that field.

// app/reviews/[slug]/actions.js

'use server';

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

export async function createCommentAction(formData) {
  const data = {
    slug: formData.get('slug'),
    user: formData.get('user'),
    message: formData.get('message'),
  };
  const error = validate(data);
  if (error) {
    return { isError: true, message: error };
  }
  const comment = await createComment(data);
  console.log('created:', comment);
  revalidatePath(`/reviews/${data.slug}`);
  redirect(`/reviews/${data.slug}`);
}

function validate(data) {
  if (!data.user) {
    return 'Name field is required';
  }
  if (data.user.length > 50) {
    return 'Name field cannot be longer than 50 characters';
  }
  if (!data.message) {
    return 'Comment field is required';
  }
  if (data.message.length > 500) {
    return 'Comment field cannot be longer than 500 characters';
  }
}

Key Concepts:

  • Data Extraction: The function extracts slug, user, and message from the formData object.

  • Validation: A validate function checks the data for required fields and length constraints. If validation fails, it returns an error message.

  • Creating a Comment: If validation is successful, the comment is created by calling createComment, which is assumed to handle database operations.

  • Revalidation and Redirection: After a successful comment creation, revalidatePath and redirect are used to refresh the page and display the new comment.

Advanced Validation Libraries

While our example uses simple validation logic, real-world applications often require more robust and flexible validation. Here are some libraries that can help:

  • Zod: A TypeScript-first schema declaration and validation library. It's great for defining complex validation schemas and ensuring type safety in your applications.

  • Formik: A popular library for building forms in React, with a strong emphasis on validation and error handling. Formik can be used alongside validation libraries like Yup to handle complex validation logic.

  • Joi: A powerful schema description language and data validator for JavaScript. It's commonly used in Node.js applications to validate request payloads and ensure data integrity.

These libraries provide more sophisticated validation capabilities and can be integrated into your Next.js applications for enhanced validation logic.

Conclusion

By effectively managing server-side validation and client-side feedback, we enhance the user experience in our Next.js applications. Users receive immediate feedback on their input, while successful submissions result in cleared form fields, ready for new data. This blend of client-side interaction and server-side validation ensures both usability and security in our web applications. For more complex validation needs, consider using libraries like Zod, Formik, or Joi to define and enforce schemas.

Last updated