Skip to main content

Overview

Server Actions allow you to execute server-side code directly from forms and client components without creating API endpoints. They provide a seamless way to handle mutations and form submissions.

Creating server actions

Define server actions in files marked with 'use server':
app/actions/form-actions.js
'use server';

import { cookies } from '#cosmos-rsc/server';

export async function contactAction(formData) {
  // Simulate server processing
  await new Promise((resolve) => setTimeout(resolve, 1000));
  
  const name = formData.get('name');
  const email = formData.get('email');
  const message = formData.get('message');
  
  // Simple validation
  if (!name || !email || !message) {
    return { success: false };
  }
  
  // Store submission timestamp in cookie
  const cookieManager = cookies();
  cookieManager.set('last_submission', new Date().toISOString(), {
    maxAge: 24 * 60 * 60, // 24 hours
    path: '/',
  });
  
  console.log('Contact form submitted:', { name, email, message });
  
  return { success: true };
}
Key points:
  • File must start with 'use server' directive
  • Functions can be async
  • Can access server resources like cookies
  • Return values must be serializable
The 'use server' directive marks the entire file as server-only code.

Using actions in forms

Pass server actions directly to form action props:
app/pages/features/forms.js
import { cookies } from '#cosmos-rsc/server';
import { contactAction } from '../../actions/form-actions';
import { SubmitButton } from '../../components/submit-button';

export default function FormsDemo() {
  const cookieManager = cookies();
  const lastSubmission = cookieManager.get('last_submission');
  
  return (
    <div>
      <h1>Server Actions Form Demo</h1>
      
      {lastSubmission && (
        <div>
          Last form submission: {new Date(lastSubmission).toLocaleString()}
        </div>
      )}
      
      <form action={contactAction} className='space-y-4'>
        <div>
          <label htmlFor='name'>Name</label>
          <input
            type='text'
            id='name'
            name='name'
            className='block w-full rounded-md border px-4 py-2'
          />
        </div>
        
        <div>
          <label htmlFor='email'>Email</label>
          <input
            type='text'
            id='email'
            name='email'
            className='block w-full rounded-md border px-4 py-2'
          />
        </div>
        
        <div>
          <label htmlFor='message'>Message</label>
          <textarea
            id='message'
            name='message'
            rows={4}
            className='block w-full rounded-md border px-4 py-2'
          ></textarea>
        </div>
        
        <SubmitButton>Submit</SubmitButton>
      </form>
    </div>
  );
}
When the form submits:
  1. Browser sends FormData to server
  2. Server decodes and executes the action
  3. Server re-renders the page with updated data
  4. Client receives new RSC payload and updates UI

Submit button with pending state

Create a client component for the submit button to show loading state:
app/components/submit-button.js
'use client';

import { useFormStatus } from 'react-dom';

export function SubmitButton({ children }) {
  const { pending } = useFormStatus();
  
  return (
    <button
      type='submit'
      disabled={pending}
      className={
        pending
          ? 'rounded bg-gray-400 px-4 py-2 text-white'
          : 'rounded bg-blue-500 px-4 py-2 text-white hover:bg-blue-600'
      }
    >
      {pending ? 'Submitting...' : children}
    </button>
  );
}
The useFormStatus hook provides:
  • pending: Whether form is currently submitting
  • data: The FormData being submitted
  • method: The HTTP method (POST)
  • action: The action URL or function
useFormStatus only works inside components that are children of a <form> element.

Calling actions from client components

You can also call server actions from event handlers:
'use client';

import { useState } from 'react';
import { contactAction } from '../actions/form-actions';

function ContactForm() {
  const [result, setResult] = useState(null);
  
  async function handleSubmit(e) {
    e.preventDefault();
    
    const formData = new FormData(e.target);
    const result = await contactAction(formData);
    setResult(result);
  }
  
  return (
    <form onSubmit={handleSubmit}>
      <input name='name' />
      <input name='email' />
      <button type='submit'>Submit</button>
      {result?.success && <p>Success!</p>}
    </form>
  );
}

Server action implementation

COSMOS RSC implements server actions using the React Server DOM protocol:

Server-side handling

core/server/index.js
const {
  decodeReplyFromBusboy,
  decodeAction,
  decodeFormState,
} = require('react-server-dom-webpack/server');

async function requestHandler(req, res) {
  let serverActionResult;
  let formState;
  
  if (req.method === 'POST') {
    const serverActionId = req.headers['server-action-id'];
    
    if (serverActionId) {
      // Programmatic action call from client
      const bb = busboy({ headers: req.headers });
      req.pipe(bb);
      
      const [fileUrl, functionName] = serverActionId.split('#');
      const serverAction = require(fileURLToPath(fileUrl))[functionName];
      const args = await decodeReplyFromBusboy(bb);
      
      serverActionResult = await serverAction.apply(null, args);
    } else {
      // Form submission
      const formData = await fakeReq.formData();
      const action = await decodeAction(formData);
      const result = await action();
      formState = await decodeFormState(result, formData);
    }
  }
  
  // Re-render page with action result
  const payload = {
    rootLayout,
    tree,
    serverActionResult,
    formState,
  };
  const rscStream = renderToPipeableStream(payload, webpackMap);
}

Client-side invocation

core/client/lib/call-server.js
export function callServer(id, args) {
  const { promise, resolve, reject } = Promise.withResolvers();
  
  dispatchAppAction({
    type: APP_ACTION.SERVER_ACTION,
    payload: { id, args },
    resolve,
    reject,
  });
  
  return promise;
}
core/client/lib/app-reducer.js
case APP_ACTION.SERVER_ACTION: {
  const { id, args } = action.payload;
  const { resolve, reject } = action;
  
  try {
    const { tree, serverActionResult } = await postServerAction(id, args);
    resolve(serverActionResult);
    
    // Update UI with new tree
    return { ...prevState, tree };
  } catch (error) {
    reject(error);
    return prevState;
  }
}
core/client/lib/post-server-action.js
export async function postServerAction(id, args) {
  const body = await encodeReply(args);
  
  const response = await fetch(window.location.pathname, {
    method: 'POST',
    headers: {
      'server-action-id': id,
    },
    body,
  });
  
  const { tree, serverActionResult } = await createFromReadableStream(
    response.body,
    { callServer }
  );
  
  return { tree, serverActionResult };
}

Action patterns

Form validation

'use server';

export async function createPost(formData) {
  const title = formData.get('title');
  const content = formData.get('content');
  
  // Validate input
  const errors = {};
  if (!title || title.length < 3) {
    errors.title = 'Title must be at least 3 characters';
  }
  if (!content || content.length < 10) {
    errors.content = 'Content must be at least 10 characters';
  }
  
  if (Object.keys(errors).length > 0) {
    return { success: false, errors };
  }
  
  // Save to database
  const post = await db.posts.create({ title, content });
  
  return { success: true, post };
}

Database mutations

'use server';

import { db } from '../lib/database';
import { revalidatePath } from '../lib/revalidate';

export async function updateProfile(userId, formData) {
  const name = formData.get('name');
  const bio = formData.get('bio');
  
  await db.users.update(userId, { name, bio });
  
  // Revalidate affected pages
  revalidatePath(`/users/${userId}`);
  
  return { success: true };
}

File uploads

'use server';

import { writeFile } from 'fs/promises';
import { join } from 'path';

export async function uploadImage(formData) {
  const file = formData.get('image');
  
  if (!file || !(file instanceof File)) {
    return { success: false, error: 'No file provided' };
  }
  
  const bytes = await file.arrayBuffer();
  const buffer = Buffer.from(bytes);
  
  const path = join(process.cwd(), 'public', 'uploads', file.name);
  await writeFile(path, buffer);
  
  return { success: true, path: `/uploads/${file.name}` };
}

Inline server actions

You can also define inline server actions:
export default function Page() {
  async function handleSubmit(formData) {
    'use server';
    
    const message = formData.get('message');
    console.log('Received:', message);
  }
  
  return (
    <form action={handleSubmit}>
      <input name='message' />
      <button type='submit'>Send</button>
    </form>
  );
}
Inline server actions must include the 'use server' directive at the top of the function body.

Progressive enhancement

Forms with server actions work without JavaScript:
<form action={contactAction}>
  <input name='email' required />
  <button type='submit'>Subscribe</button>
</form>
Behavior:
  • With JavaScript: Form submits asynchronously, UI updates smoothly
  • Without JavaScript: Form submits as traditional POST, page reloads with new data

Error handling

Handle errors in server actions:
'use server';

export async function dangerousAction(formData) {
  try {
    const result = await riskyOperation();
    return { success: true, result };
  } catch (error) {
    console.error('Action failed:', error);
    return { success: false, error: error.message };
  }
}
Display errors in the UI:
'use client';

export function ActionForm() {
  const [result, setResult] = useState(null);
  
  async function handleAction(formData) {
    const result = await dangerousAction(formData);
    setResult(result);
  }
  
  return (
    <form action={handleAction}>
      {result?.error && (
        <div className='text-red-600'>{result.error}</div>
      )}
      <button type='submit'>Submit</button>
    </form>
  );
}

Security considerations

Never trust client input. Always validate and sanitize data in server actions.
Verify the user has permission to perform the action before executing it.
Implement rate limiting to prevent abuse of server actions.
React Server Actions include built-in CSRF protection through the action encoding mechanism.

Next steps

Server Components

Learn about React Server Components

Routing

Understand file-system routing

Build docs developers (and LLMs) love