Skip to main content

Calling Server Actions from React: The Basics

You call a Server Action in three main ways: as a direct function call in a Client Component or event handler, by passing it to an HTML form's action prop for automatic form submission, or via the useFormStatus and useActionState hooks for more control over pending state and errors. Each method has a use case—choose based on whether you're building a traditional form, a button that triggers an action, or a component that needs to track submission state.

The simplest approach is passing a Server Action to a <form action={action}>. This works immediately, without JavaScript, because HTML forms natively POST to URLs. Next.js converts your Server Action into an endpoint, so the form submission "just works." When JavaScript loads, React's form handling takes over and can add polish like optimistic updates and loading states.

Direct Function Calls

You can call a Server Action directly from a Client Component using await, just like any async function. Next.js transparently sends the call to the server.

// app/actions.ts
'use server';

export async function incrementCounter(count: number) {
return count + 1;
}
// app/components/Counter.tsx
'use client';

import { incrementCounter } from '@/app/actions';

export default function Counter() {
const [count, setCount] = React.useState(0);

async function handleClick() {
try {
const newCount = await incrementCounter(count);
setCount(newCount);
} catch (error) {
console.error('Failed:', error);
}
}

return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>Increment</button>
</div>
);
}

When you click the button, incrementCounter(count) is sent to the server, executed, and the result is returned. The async call is transparent: you write it as if you're calling a local function, but it runs on the server.

Using Form Actions

Passing a Server Action to a form's action prop is the most powerful pattern because it works without JavaScript. The form automatically serializes all input values and sends them to the Server Action.

// app/components/AddPostForm.tsx
'use client';

import { createPost } from '@/app/actions';

export function AddPostForm() {
return (
<form action={createPost}>
<input
type="text"
name="title"
placeholder="Post title"
required
/>
<textarea
name="content"
placeholder="Write something..."
required
></textarea>
<button type="submit">Publish</button>
</form>
);
}
// app/actions.ts
'use server';

import { db } from '@/lib/db';
import { revalidatePath } from 'next/cache';

export async function createPost(formData: FormData) {
const title = formData.get('title') as string;
const content = formData.get('content') as string;

const post = await db.posts.create({ title, content });

revalidatePath('/blog');
return { success: true, postId: post.id };
}

The form works immediately. Without JavaScript, the browser submits to the Server Action endpoint, and the page reloads with the new post. With JavaScript, React captures the submission, preventing the reload, and you can add optimistic updates (see article 6).

Tracking Submission State with useFormStatus

The useFormStatus hook (from React 19) gives you access to the form's submission state. It returns an object with pending, data (FormData), method, and action.

// app/components/SubmitButton.tsx
'use client';

import { useFormStatus } from 'react-dom';

export function SubmitButton() {
const status = useFormStatus();

return (
<button type="submit" disabled={status.pending}>
{status.pending ? 'Saving...' : 'Save'}
</button>
);
}

Use this inside a form to show loading state, disable the button while submitting, or display a spinner. The hook works with any Server Action passed to the form's action prop.

Advanced: useActionState Hook

The useActionState hook (React 19) combines form submission with state management. It wraps a Server Action and gives you the action's response, pending state, and a form action to bind to your form.

// app/components/UpdateProfileForm.tsx
'use client';

import { useActionState } from 'react';
import { updateProfile } from '@/app/actions';

export function UpdateProfileForm() {
const [state, formAction, isPending] = useActionState(updateProfile, null);

return (
<form action={formAction}>
<input type="text" name="name" placeholder="Name" required />
<button type="submit" disabled={isPending}>
{isPending ? 'Updating...' : 'Update'}
</button>
{state?.error && <p style={{ color: 'red' }}>{state.error}</p>}
{state?.success && <p style={{ color: 'green' }}>Updated!</p>}
</form>
);
}
// app/actions.ts
'use server';

export async function updateProfile(prevState: any, formData: FormData) {
const name = formData.get('name') as string;

if (!name) {
return { error: 'Name is required', success: false };
}

// Update logic
return { success: true, error: null };
}

The action function receives the previous state as the first argument and FormData as the second. It returns a new state object that useActionState exposes to your component. This pattern is ideal for showing error messages or success confirmations without additional useEffect or useState hooks.

Error Handling in Server Actions

When a Server Action throws an error, the error is serialized and sent to the client. You can catch it with try/catch.

// app/components/DeleteButton.tsx
'use client';

import { deleteItem } from '@/app/actions';

export function DeleteButton({ id }: { id: string }) {
async function handleDelete() {
try {
await deleteItem(id);
console.log('Deleted successfully');
} catch (error) {
console.error('Delete failed:', error instanceof Error ? error.message : String(error));
}
}

return <button onClick={handleDelete}>Delete</button>;
}

For form-based actions, use useActionState to capture and display errors without try/catch:

const [state, formAction] = useActionState(deleteItem, { error: null });
// In JSX:
{state?.error && <p>{state.error}</p>}

Comparison: Direct Call vs Form Action vs useActionState

MethodUse CaseWorks without JSTracks PendingHandles Errors Easily
Direct call (onClick)Button click, custom logicNoNo (use useState)Yes (try/catch)
Form action (<form action={}>)Traditional form, progressive enhancementYesWith useFormStatusManual
useActionStateForm with error/success stateYesBuilt-inBuilt-in

Choose form actions when you want progressive enhancement. Use useActionState when you need to display errors or success messages. Use direct calls for one-off mutations like a like button or delete confirmation.

Key Takeaways

  • Call a Server Action directly from a Client Component using await, like any async function.
  • Pass a Server Action to a form's action prop to enable progressive enhancement—the form works without JavaScript.
  • Use useFormStatus to show pending state (loading spinner, disabled button) during form submission.
  • Use useActionState to combine form submission with state management and built-in error/success handling.
  • Server Actions automatically serialize FormData into the arguments your action receives.

Frequently Asked Questions

Can I pass multiple arguments to a Server Action from a form?

Yes. FormData converts all input names to key/value pairs. In your Server Action, extract each value using formData.get(name). Alternatively, you can call a Server Action directly from a click handler with multiple arguments: await action(arg1, arg2, arg3).

What happens if the server is offline when I call a Server Action?

The client-side call will timeout (default 30 seconds in production). You should wrap calls in try/catch and show an error message. In development, you'll see a more detailed error; in production, you get a generic timeout error for security.

Can I call a Server Action from a Server Component?

Yes. In a Server Component, you can call a Server Action directly during server-side rendering. The logic runs on the server, and the result is available immediately (no network round-trip). This is useful for initial data fetching.

How do I pass complex objects to a Server Action?

Complex objects (like nested objects or Date instances) can be passed directly. Next.js serializes them using a superset of JSON that includes Date, Map, Set, and other types. Avoid functions and class instances, which cannot be serialized.

Does the form action block the default browser behavior?

No—without JavaScript, the form submits normally, and the browser reloads. With JavaScript, React's form handling prevents the default submission and processes it via the Server Action. After the action completes, you can redirect (using redirect()) or update the UI optimistically.

Further Reading