DevPilot
web-development·September 21, 2026·5 min read·Updated Sep 23, 2026

Building React Components with Server Actions in Next.js

How to structure React components that use Next.js server actions for data mutations, with concrete file structure, prerequisites, verification steps, failure modes, and security notes.

reactnextjsserver-actionscomponents

Building React Components with Server Actions in Next.js

Server actions allow forms and mutations to call server-side code directly from React components without exposing endpoints publicly. This article explains the file structure, prerequisites, a working pattern, common failure modes, verification steps, and security considerations.

Prerequisites

  • Next.js 15+ with App Router (app/ directory)
  • React 19 (server actions rely on React transitions for loading/error states)
  • TypeScript recommended (not required)
  • A component that needs direct server mutation (database update, authentication check, file operation, or authentication flow)

This pattern is appropriate when the mutation must access server-side resources (database, authentication session, file system) and should not be reconstructed purely from client-side state.

Problem

Some mutations should not rely on client-side state alone. Examples:

  • A form that updates a database record requires a server-side database connection (the browser has no direct database access).
  • A mutation that checks an authentication session must read the session cookie server-side (client-side session access is unreliable and insecure).
  • A file upload that writes to a server filesystem requires server-side file system access.

Using a public API endpoint for these cases introduces extra complexity (route handlers, CORS, public exposure). Server actions provide a simpler alternative: the mutation runs server-side, called directly from the component.

Working approach

Step 1: Create the server action file

Create a file with the "use server" directive at the top. The directive tells Next.js to treat the exports as server-side functions callable from components:

// app/actions/update.ts
"use server";

import { revalidatePath } from "next/cache";

export async function updateRecord(id: string, data: Record<string, unknown>) {
  // Server-side mutation logic (database, auth, filesystem)
  // This runs on the server only; not exposed as a public endpoint.
  // After mutation, optionally revalidate: revalidatePath("/");
  return { success: true, id };
}

Step 2: Import and call from a component

Import the server action into a component (client or server component — server actions work from both in the App Router) and bind it to a form or interaction:

// app/components/UpdateForm.tsx
"use client";

import { useTransition } from "react";
import { updateRecord } from "@/app/actions/update";

export default function UpdateForm({ id }: { id: string }) {
  const [isPending, startTransition] = useTransition();
  const [error, setError] = React.useState<string | null>(null);

  async function handleSubmit(formData: FormData) {
    startTransition(async () => {
      try {
        setError(null);
        await updateRecord(id, Object.fromEntries(formData));
      } catch (e) {
        setError(e instanceof Error ? e.message : "Unknown error");
      }
    });
  }

  return (
    <form action={handleSubmit}>
      <input name="title" defaultValue="Initial value" />
      <button type="submit" disabled={isPending}>
        {isPending ? "Saving..." : "Save"}
      </button>
      {error && <p role="alert" className="text-sm text-red-600">{error}</p>}
    </form>
  );
}

Step 3: Handle loading and error states

Always use React useTransition() for mutations that trigger server actions. This ensures the UI responds correctly to pending mutations (disable submit buttons, show loading indicators, prevent duplicate submissions). Always show error messages when a mutation fails; never suppress errors silently.

Step 4: Structure for maintainability

Keep server actions in dedicated files (e.g., app/actions/) rather than inline in components. This makes the mutation boundary explicit, easier to test, and easier to audit.

Verification

To verify the server action works correctly:

  1. Run the site locally (npm run dev).
  2. Submit the form. Confirm the mutation executes (check database, log, or server response).
  3. Confirm the UI shows a loading state (isPending is true during mutation).
  4. Confirm errors display correctly (trigger a failure intentionally — for example, submit invalid data or disconnect the database — and verify the error message appears).
  5. Confirm no public API endpoint is exposed for the mutation (check that no route handler exists for the mutation path; the mutation is only callable via the component import).

Common failure modes

  • Missing "use server": the file exports a standard function; calling it from a component produces a build-time or runtime error.
  • Calling the server action from a server component without importing it correctly: the function must be imported by name from the module that exports it with "use server".
  • Not handling errors: if the mutation throws, the component must catch the error; otherwise the user sees no feedback.
  • Not using useTransition: without it, the form submits without a loading indicator; duplicate submissions are possible.
  • Exposing mutations publicly by mistake: if the file exports a standard endpoint (API route) in addition to the server action, the mutation becomes publicly callable; keep mutations in dedicated "use server" files only.

Security considerations

  • Server actions run server-side; never expose them as public API endpoints unintentionally.
  • Validate all input in the server action (database queries must use parameterized statements; file paths must be validated; authentication checks must occur server-side, not only client-side).
  • Never trust client-side validation alone; the server action must enforce its own validation rules.
  • Do not include secrets, tokens, or environment variables in component code; the server action has access to server-side environment variables; this is a feature, not a risk, as long as input validation is enforced.

Caveats

  • Server actions are not a replacement for public APIs; they are appropriate for mutations that should remain server-bound.
  • Large payloads or file uploads require separate handling (streaming/upload APIs) rather than direct form submission through server actions.
  • Server actions must not modify content solely for search optimization or keyword capture; this violates the publication's quality philosophy.

Related articles

  • See web-development/deployment/deploy-nextjs-vercel-env for environment-related deployment differences that affect server-side execution (database connections differ between local persistent servers and serverless execution).
  • See troubleshooting/databases/database-connection-production for diagnosing database connection failures in server actions (same root cause: differences between local and production execution environments).