Skip to main content
COSMOS RSC supports React Server Actions, allowing you to handle form submissions directly on the server without building separate API endpoints.

Creating a server action

1

Create an actions file

Create a file in app/actions/ and mark it with 'use server':
app/actions/form-actions.js
'use server';

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

export async function contactAction(formData) {
  // Extract form data
  const name = formData.get('name');
  const email = formData.get('email');
  const message = formData.get('message');

  // Validate
  if (!name || !email || !message) {
    return { success: false, error: 'All fields are required' };
  }

  // Process the form (save to database, send email, etc.)
  console.log('Contact form submitted:', { name, email, message });

  // Update cookies
  const cookieManager = cookies();
  cookieManager.set('last_submission', new Date().toISOString(), {
    maxAge: 24 * 60 * 60, // 24 hours
    path: '/',
  });

  return { success: true };
}
2

Create a form page

Import and use the action in a form:
app/pages/contact.js
import { contactAction } from '../actions/form-actions';
import { SubmitButton } from '../components/submit-button';

export default function ContactPage() {
  return (
    <div className='mx-auto max-w-lg py-12'>
      <h1 className='mb-8 text-3xl font-bold'>Contact Us</h1>

      <form action={contactAction} className='space-y-4'>
        <div>
          <label htmlFor='name' className='block text-sm font-medium'>
            Name
          </label>
          <input
            type='text'
            id='name'
            name='name'
            className='mt-1 block w-full rounded-md border px-4 py-2'
            required
          />
        </div>

        <div>
          <label htmlFor='email' className='block text-sm font-medium'>
            Email
          </label>
          <input
            type='email'
            id='email'
            name='email'
            className='mt-1 block w-full rounded-md border px-4 py-2'
            required
          />
        </div>

        <div>
          <label htmlFor='message' className='block text-sm font-medium'>
            Message
          </label>
          <textarea
            id='message'
            name='message'
            rows={4}
            className='mt-1 block w-full rounded-md border px-4 py-2'
            required
          />
        </div>

        <SubmitButton>Submit</SubmitButton>
      </form>
    </div>
  );
}
3

Add a submit button with pending state

Create a client component that shows loading state:
app/components/submit-button.js
'use client';

import { useFormStatus } from 'react-dom';

export function SubmitButton({ children, ...props }) {
  const { pending } = useFormStatus();

  return (
    <button
      type='submit'
      className='rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 disabled:opacity-50'
      disabled={pending}
      {...props}
    >
      {pending ? (
        <span className='flex items-center gap-2'>
          <svg
            className='h-5 w-5 animate-spin'
            xmlns='http://www.w3.org/2000/svg'
            fill='none'
            viewBox='0 0 24 24'
          >
            <circle
              className='opacity-25'
              cx='12'
              cy='12'
              r='10'
              stroke='currentColor'
              strokeWidth='4'
            />
            <path
              className='opacity-75'
              fill='currentColor'
              d='M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z'
            />
          </svg>
          Submitting...
        </span>
      ) : (
        children
      )}
    </button>
  );
}

Complete form example

Here’s the full form demo from the COSMOS RSC source:
app/pages/features/forms.js
import { cookies } from '#cosmos-rsc/server';
import { NavigationTransition } from '../../components/navigation-transition';
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 className='mx-auto max-w-4xl px-4 py-12'>
      <NavigationTransition>
        <h1 className='mb-8 text-3xl font-bold'>Server Actions Form Demo</h1>
      </NavigationTransition>

      <a href='/' className='text-blue-500 hover:underline'>
        Back to Home
      </a>

      <div className='space-y-8'>
        {lastSubmission && (
          <div className='text-sm text-gray-600'>
            Last form submission: {new Date(lastSubmission).toLocaleString()}
          </div>
        )}

        <section>
          <h2 className='mb-4 text-xl font-semibold'>Contact Form Example</h2>
          <p className='mb-6 text-gray-700'>
            This form demonstrates server actions by handling form submissions
            on the server without client-side JavaScript.
          </p>

          <form action={contactAction} className='max-w-lg space-y-4'>
            <div>
              <label
                htmlFor='name'
                className='block text-sm font-medium text-gray-700'
              >
                Name
              </label>
              <input
                type='text'
                id='name'
                name='name'
                className='mt-1 block w-full rounded-md border-gray-300 px-4 py-2 shadow-sm'
              />
            </div>

            <div>
              <label
                htmlFor='email'
                className='block text-sm font-medium text-gray-700'
              >
                Email
              </label>
              <input
                type='text'
                id='email'
                name='email'
                className='mt-1 block w-full rounded-md border-gray-300 px-4 py-2 shadow-sm'
              />
            </div>

            <div>
              <label
                htmlFor='message'
                className='block text-sm font-medium text-gray-700'
              >
                Message
              </label>
              <textarea
                id='message'
                name='message'
                rows={4}
                className='mt-1 block w-full rounded-md border-gray-300 px-4 py-2 shadow-sm'
              ></textarea>
            </div>

            <SubmitButton>Submit</SubmitButton>
          </form>
        </section>
      </div>
    </div>
  );
}

Working with FormData

Server actions receive a FormData object. Extract values using get() or getAll():
'use server';

export async function submitAction(formData) {
  // Single value
  const name = formData.get('name');

  // Multiple values (checkboxes)
  const interests = formData.getAll('interests');

  // File upload
  const file = formData.get('avatar');

  return { success: true };
}

Validation and error handling

Return validation errors from your action:
'use server';

export async function submitAction(formData) {
  const email = formData.get('email');

  if (!email || !email.includes('@')) {
    return {
      success: false,
      errors: { email: 'Please enter a valid email address' }
    };
  }

  // Process valid data
  await saveToDatabase({ email });

  return { success: true };
}
Display errors in your form:
'use client';

import { useFormState } from 'react-dom';
import { submitAction } from '../actions/form-actions';

export function ContactForm() {
  const [state, formAction] = useFormState(submitAction, null);

  return (
    <form action={formAction}>
      <input type='email' name='email' />
      {state?.errors?.email && (
        <p className='text-red-600'>{state.errors.email}</p>
      )}
      <button type='submit'>Submit</button>
    </form>
  );
}

Using server APIs

Server actions can access server-only APIs:
'use server';

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

export async function updatePreferences(formData) {
  const theme = formData.get('theme');

  // Set cookie
  const cookieManager = cookies();
  cookieManager.set('theme', theme, {
    maxAge: 365 * 24 * 60 * 60, // 1 year
    path: '/',
  });

  return { success: true };
}

Progressive enhancement

Forms with server actions work without JavaScript enabled. The browser will fall back to a standard form submission:
<form action={contactAction} method='POST'>
  {/* Form fields */}
  <button type='submit'>Submit</button>
</form>
With JavaScript enabled, React intercepts the submission and handles it with the server action, providing a better user experience.

Best practices

Never trust client-side data. Always validate in your server action, even if you also validate on the client.
Return structured data from actions to provide feedback:
return { success: true, message: 'Form submitted!' };
Wrap database operations in try/catch:
try {
  await saveToDatabase(data);
  return { success: true };
} catch (error) {
  console.error(error);
  return { success: false, error: 'Failed to save' };
}
Create separate action functions for different forms rather than one large handler:
export async function loginAction(formData) { /* ... */ }
export async function signupAction(formData) { /* ... */ }
export async function resetPasswordAction(formData) { /* ... */ }

Build docs developers (and LLMs) love