Skip to content
Codeloom
React

React Server Functions Explained

Learn how Server Functions with 'use server' let you call backend logic directly from React components without building API routes.

·5 min read · By Codeloom
Intermediate 11 min read

What you'll learn

  • What Server Functions are and how they work
  • The use server directive and its placement
  • Calling Server Functions from client and server components
  • Validation and security considerations
  • Patterns for revalidation and redirects

Prerequisites

  • Basic React knowledge
  • Understanding of client vs server components

Server Functions let you define backend logic in the same file or module as your React components and call it as if it were a regular function. Behind the scenes, the framework serializes the arguments, sends them to the server over HTTP, executes the function, and sends back the result. You never write a fetch call, an API route, or a request handler.

The use server Directive

You mark a function as a Server Function by adding "use server" as its first line. This tells the bundler to exclude the function body from the client bundle and expose it as an RPC endpoint instead.

async function addTodo(formData) {
  'use server';
  const title = formData.get('title');
  await db.todos.insert({ title, completed: false });
}

You can also mark an entire module as server-only by placing "use server" at the top of the file. Every exported function in that module becomes a Server Function.

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

export async function createUser(formData) {
  const email = formData.get('email');
  const name = formData.get('name');
  return await db.users.create({ email, name });
}

export async function deleteUser(userId) {
  await db.users.delete(userId);
}

Calling from Forms

The most natural way to use a Server Function is as a form action. Pass it directly to the action prop of a <form>. The browser submits the form, React intercepts it, and calls your function with the FormData.

import { createUser } from './actions';

export default function SignupPage() {
  return (
    <form action={createUser}>
      <input name="name" placeholder="Name" required />
      <input name="email" type="email" placeholder="Email" required />
      <button type="submit">Create Account</button>
    </form>
  );
}

This works even before JavaScript loads on the client, giving you progressive enhancement for free.

Calling from Event Handlers

Server Functions are not limited to forms. You can call them from any event handler in a Client Component by importing them and invoking them with await.

'use client';
import { deleteUser } from './actions';

export function DeleteButton({ userId }) {
  const handleDelete = async () => {
    const confirmed = window.confirm('Delete this user?');
    if (confirmed) {
      await deleteUser(userId);
    }
  };

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

When you call a Server Function this way, React serializes the arguments using a protocol similar to JSON but capable of handling more types (Dates, FormData, typed arrays, and more). The function runs on the server and the result is sent back.

Argument Serialization

Server Functions accept serializable arguments. You can pass strings, numbers, booleans, arrays, plain objects, Date instances, FormData, and even other Server Functions as callbacks. You cannot pass class instances, DOM elements, or functions that are not themselves Server Functions.

'use server';

export async function updateSettings(userId, settings) {
  // userId is a string, settings is a plain object
  await db.settings.upsert({
    userId,
    theme: settings.theme,
    notifications: settings.notifications,
    updatedAt: new Date(),
  });
}
'use client';
import { updateSettings } from './actions';

function SettingsForm({ userId }) {
  const handleSave = async (e) => {
    e.preventDefault();
    await updateSettings(userId, {
      theme: 'dark',
      notifications: true,
    });
  };

  return (
    <form onSubmit={handleSave}>
      {/* form fields */}
      <button type="submit">Save</button>
    </form>
  );
}

Security: Always Validate

A Server Function is a public endpoint. Anyone can call it with any arguments. Never trust the input. Always validate and authorize.

'use server';
import { getSession } from '@/lib/auth';
import { z } from 'zod';

const PostSchema = z.object({
  title: z.string().min(1).max(200),
  body: z.string().min(1).max(10000),
});

export async function createPost(formData) {
  const session = await getSession();
  if (!session) {
    throw new Error('Unauthorized');
  }

  const parsed = PostSchema.safeParse({
    title: formData.get('title'),
    body: formData.get('body'),
  });

  if (!parsed.success) {
    return { errors: parsed.error.flatten().fieldErrors };
  }

  const post = await db.posts.create({
    ...parsed.data,
    authorId: session.userId,
  });

  return { post, errors: null };
}

Revalidation After Mutations

After a Server Function mutates data, you often want to refresh the UI. In Next.js, you can call revalidatePath or revalidateTag to bust the cache for specific routes or data.

'use server';
import { revalidatePath } from 'next/cache';
import { redirect } from 'next/navigation';

export async function publishArticle(articleId) {
  await db.articles.update(articleId, { status: 'published' });
  revalidatePath('/articles');
  redirect(`/articles/${articleId}`);
}

The revalidatePath call tells the framework to re-render the /articles page with fresh data. The redirect sends the user to the published article.

Combining with useActionState

For forms that need pending indicators and progressive enhancement, combine Server Functions with useActionState.

'use client';
import { useActionState } from 'react';
import { createPost } from './actions';

export function NewPostForm() {
  const [state, action, isPending] = useActionState(createPost, {
    errors: null,
    post: null,
  });

  return (
    <form action={action}>
      <input name="title" />
      {state.errors?.title && <span>{state.errors.title}</span>}
      <textarea name="body" />
      {state.errors?.body && <span>{state.errors.body}</span>}
      <button disabled={isPending}>
        {isPending ? 'Publishing...' : 'Publish'}
      </button>
    </form>
  );
}

Closures and Bound Arguments

When you define a Server Function inside a Server Component, it can close over server-side data. React serializes those closed-over values and sends them along when the client calls the function.

// Server Component
export default async function TodoItem({ todo }) {
  async function toggleComplete() {
    'use server';
    // `todo.id` is captured from the server-side closure
    await db.todos.update(todo.id, { completed: !todo.completed });
    revalidatePath('/todos');
  }

  return (
    <form action={toggleComplete}>
      <span>{todo.title}</span>
      <button type="submit">{todo.completed ? 'Undo' : 'Done'}</button>
    </form>
  );
}

Be cautious with closures. The closed-over values are embedded in the HTML sent to the client. Never close over secrets, tokens, or sensitive data.

When to Use Server Functions

Use Server Functions whenever you need to mutate data from the UI. They replace API routes for most CRUD operations. For read operations, prefer Server Components that fetch data directly. For real-time data, WebSockets or server-sent events are still the right tool. Server Functions are the mutation primitive for modern React.