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.
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:
Browser sends FormData to server
Server decodes and executes the action
Server re-renders the page with updated data
Client receives new RSC payload and updates UI
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
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
'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