Part 107: Structuring Server Actions in Next.js: Enhancing Application Security and Flexibility

[App] Server Actions

[App] Server Actions

When building secure and efficient web applications, validating input data on the server side is crucial, even if client-side validation is in place. In our previous setup, we ensured that our Server Action returns an error object if the "user" value is missing. However, this setup required some client-side logic to handle error responses, which led to an interesting challenge: how to manage server actions in a way that's compatible with both Server and Client Components.

In this post, we'll explore how to refactor our Next.js application to separate server actions into distinct files, enhancing our app's structure and maintainability. This approach aligns with Next.js best practices and improves our ability to handle both server-side and client-side logic seamlessly.

The Challenge with Server Actions in Client Components

When attempting to use a Server Action in a Client Component, you might encounter an error indicating that inline Server Actions are not allowed within Client Components. To resolve this, you can either pass the action function as a prop or, more elegantly, define the Server Action in a separate file.

Next.js documentation highlights two primary locations for Server Actions:

  1. Inside the Component: This works only if the component is a Server Component.

  2. In a Separate File: This approach is versatile, allowing use in both Server and Client Components.

Let's delve into how to implement the latter solution by creating a separate file for our Server Action.

Creating a Separate File for Server Actions

To begin, we'll create a new file for our server action. This file can be located anywhere within your project, but it's often convenient to place it near the component or route that utilizes it. In our example, we'll create an actions.js file in the same directory as the ReviewPage.

Step-by-Step Implementation

  1. Create the Action File: Create a new file named actions.js within the appropriate directory.

    // 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) {
      if (!formData.get('user')) {
        return { isError: true, message: 'Name field is required' };
      }
      const data = {
        slug: formData.get('slug'),
        user: formData.get('user'),
        message: formData.get('message'),
      };
      const comment = await createComment(data);
      console.log('created:', comment);
      revalidatePath(`/reviews/${data.slug}`);
      redirect(`/reviews/${data.slug}`);
    }
  2. Update the Component: Modify the CommentForm component to utilize the newly created action.

    // components/CommentForm.jsx
    
    'use client';
    
    import { createCommentAction } from '@/app/reviews/[slug]/actions';
    
    export default function CommentForm({ slug, title }) {
      return (
        <form action={createCommentAction}
          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" 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>
      );
    }
    • Hidden Field: Add a hidden input field to include the slug in the form data. This field ensures that the slug is available when the form is submitted.

  3. Verify Functionality: Test the application to ensure that the server action works correctly, and comments are being processed with all necessary values.

Conclusion

By separating server actions into distinct files, you improve the organization and flexibility of your Next.js application. This approach allows you to reuse server actions across different components, whether they are Server or Client Components. Moreover, it aligns with best practices for maintaining clean and maintainable code, making it easier to handle server-side logic and client-side interactions efficiently.

In the next steps of our development process, we'll focus on handling errors returned by the Server Action and enhancing the user experience by providing informative feedback when form submissions fail. Stay tuned as we continue to refine our application!

Last updated