| |

React 113 ⚛️ React Server Functions and Wire Format

⏱️ Estimated read: 16 min | |

A Server Function is an async function marked with "use server" that a Client Component can call over the network. The client never receives the function’s code; it receives a reference, and when it invokes that reference, React sends a request to the server, executes the function there, and serializes the result back . The wire format that carries these calls is React’s Flight protocol, a streaming serialization format that encodes not just the function reference and its arguments, but the entire React element tree that the server produces.

This chapter covers the mechanics of Server Functions and the wire format beneath them. It begins with the "use server" directive and the server-reference object that the bundler creates, then explains the two ways a Client Component obtains that reference: through props from a Server Component, or through a direct import. The chapter then examines the Flight protocol’s encoding rules—the $-prefixed type tags, the chunk-based structure, and the special handling of client component references—and the serialization constraints that determine what can cross the boundary. The final sections address the security implications of client-controlled arguments and the distinction between Server Functions and the older “Server Actions” terminology.


Key point: The client does not receive a function. It receives an object shaped like {$$typeof: Symbol.for("react.server.reference"), $$id: '...'}, and calling that object triggers a network request. The $$id is the only piece of the function that crosses the wire.


Why Server Functions exist

The client-server function gap. Before Server Functions, calling server code from a Client Component required an API route and a fetch call. The developer wrote an endpoint, serialized the request body, parsed it on the server, and serialized the response. Server Functions eliminate that boilerplate: the function is defined once, marked with "use server", and the framework generates the endpoint and the serialization automatically . The call site looks like a function call, and the network boundary is hidden.

The progressive enhancement requirement. A form that submits to a Server Function works before JavaScript loads. React supplies the form’s FormData as the first argument and submits the form natively when the JS bundle is unavailable . This is not a client-side fetch that degrades gracefully; it is a form submission that React intercepts when JavaScript is present and leaves alone when it is not. The server reference is embedded in the HTML, and the browser’s native form submission posts to the same endpoint.

The serialization constraint. Not everything can cross the network boundary. Functions, class instances, React elements, and non-global symbols are rejected . The Flight protocol defines exactly what is supported: primitives, Date, Map, Set, typed arrays, FormData, plain objects, and other Server Functions. This constraint is not an arbitrary limitation; it is what makes the protocol safe and the payload reconstructable on both sides.

The naming clarification. Until September 2024, the React documentation called all Server Functions “Server Actions” . The terminology changed because the same mechanism serves two purposes: a Server Function passed to an action prop or called inside an action is a Server Action, but a Server Function used for other purposes—an event handler, a programmatic call—is not. The underlying mechanism is identical; the name depends on the context.


a. The “use server” directive and the server reference

The directive marks an async function as callable from the client. It can appear at the top of a function body or at the top of a module, in which case every export of that module becomes a Server Function . The directive must be a string literal with single or double quotes, placed before any imports or other code, and it only works in server-side files.

// A Server Function defined inside a Server Component.
// The reference is passed to the Client Component as a prop.
export default function NoteEditor() {
  async function createNote(formData: FormData) {
    'use server';
    await db.notes.create({ title: formData.get('title') as string });
  }

  return <NoteForm action={createNote} />;
}

When React renders the Server Component, the framework creates a reference object for createNote and passes it to NoteForm. The reference has a specific shape that the client can inspect:

// Inside the Client Component, the prop is not a function.
// It is a server reference object.
'use client';

export function NoteForm({ action }: { action: (data: FormData) => Promise<void> }) {
  console.log(action);
  // {$$typeof: Symbol.for("react.server.reference"), $$id: 'createNote'}
  return <form action={action}>...</form>;
}

The $$id is the identifier that the server uses to locate the function when the client calls it. The client does not have the function’s source, its closure, or its module. It has an ID and the knowledge that calling it sends a request .

A Server Function can also be imported directly into a Client Component from a module marked with "use server" at the top. In that case, the bundler replaces the import with a reference object at build time, and the call site is identical to a normal function call .

// actions.ts
'use server';

export async function deleteNote(id: string) {
  await db.notes.delete({ where: { id } });
}

// Client Component
'use client';
import { deleteNote } from './actions';

export function DeleteButton({ id }: { id: string }) {
  return <button onClick={() => deleteNote(id)}>Delete</button>;
}

The direct import form is the one that requires the "use server" directive at the module level. The inline form inside a Server Component does not need a separate module, but it cannot be imported by a Client Component; it can only be passed as a prop.


b. The Flight protocol wire format

The Flight protocol is the serialization format that carries Server Component trees and Server Function calls. It is a chunked format: each piece of data is prefixed with a chunk ID and a tag, followed by a newline and the chunk’s content. The format is designed for streaming, so chunks can arrive out of order and be assembled by the client .

The protocol uses a $-prefixed type system to represent values that JSON cannot express. A string starting with $ is escaped with a second $, so the literal string "$100" becomes "$$100" on the wire. The type tags include:

TagTypeExample
$DDate$D2025-01-15T10:30:00.000Z
$nBigInt$n99999999999999999
$SGlobal symbol$Smy.test.symbol
$undefinedundefined$undefined
$NaNNaN$NaN
$QMap reference$Q3
$WSet reference$W4

Maps and Sets are serialized as separate chunks and referenced from the parent by chunk ID. A Map’s content is an array of key-value pairs; a Set’s is an array of values . Typed arrays use a binary row format: <chunk id>:<tag><length in hex>,<raw bytes>. For example, 3:o5,Hello is chunk 3, a Uint8Array of length 5, containing the bytes for “Hello” .

React elements are serialized as arrays in the form ["$", type, key, props], where "$" represents the React element type, type is the element type (a string like "div" or a client reference), key is the key or null, and props is the props object . Client Components are not executed on the server; instead, the server serializes an Import chunk (tag I) that contains the module ID, the chunks to load, and the export name. The parent element references this import chunk with a $L prefix, which tells the client to wrap the component in React.lazy .

// Example: serializing a Server Component that renders a Client Counter.
1:I{"id":"./src/Counter.js","chunks":["chunk-abc"],"name":"Counter"}
0:["$","div",null,{"children":[["$","h1",null,{"children":"My Page"}],["$","$L1",null,{}]]}]

Chunk 1 is the import chunk for Counter. Chunk 0 is the element tree: a div containing an h1 and a lazy reference to chunk 1. The client loads the chunk, resolves the Counter module, and renders it .

For Server Function calls, the wire format carries the function’s $$id and the serialized arguments. The response carries the serialized return value, subject to the same type constraints. The Flight protocol is also used for the initial page load, where the server streams the entire RSC tree as chunks that the client hydrates.


c. Serialization constraints and security

Not every value can cross the Server Function boundary. The supported types are primitives (string, number, bigint, boolean, undefined, null, global symbols), iterables (String, Array, Map, Set, TypedArray, ArrayBuffer), Date, FormData, plain objects with serializable properties, other Server Functions, and Promises . The unsupported types are equally important: React elements, non-Server-Function functions, class instances, null-prototype objects, non-global symbols, and event objects are rejected .

These constraints exist because the protocol must reconstruct the values on both sides. A class instance has a prototype and methods that cannot be serialized; a React element is a description of UI that the Flight protocol handles through its own element encoding, not as an argument. The practical consequence is that Server Function arguments should be plain data: IDs, strings, numbers, and simple objects.

The security model follows from the wire. Every argument to a Server Function is fully controlled by the client. The client can send any payload to any exposed Server Function endpoint. The React documentation is explicit: treat arguments as untrusted input, validate and escape them, and authorize every mutation . A Server Function that deletes a note by ID must verify that the caller owns the note; the function’s existence on the server does not imply that every caller is permitted to invoke it.

'use server';

export async function deleteNote(id: string) {
  // The id came from the client. It is untrusted.
  const session = await getSession();
  const note = await db.notes.findUnique({ where: { id } });

  // Authorization: the caller must own the note.
  if (!note || note.userId !== session.userId) {
    throw new Error('Not authorized');
  }

  await db.notes.delete({ where: { id } });
}

The experimental taint APIs (experimental_taintUniqueValue, experimental_taintObjectReference) exist to prevent sensitive values from being accidentally serialized and sent to the client . They are not a substitute for authorization; they are a guard against leaking data through return values.


Complete Example Session

// ============================================
// PART 1: THE MODULE-LEVEL DIRECTIVE
// ============================================
// Every export of this file is a Server Function.
// The directive must be before any imports.

'use server';

// ============================================
// PART 2: THE SERVER FUNCTION
// ============================================
// Async, because the network call is asynchronous.
// The argument is validated before use.

export async function updateUsername(formData: FormData) {
  const name = formData.get('name') as string;

  if (!name || name.length < 3) {
    return { error: 'Name must be at least 3 characters' };
  }

  const session = await getSession();
  await db.users.update({
    where: { id: session.userId },
    data: { name },
  });

  return { success: true };
}

// ============================================
// PART 3: THE CLIENT IMPORT
// ============================================
// The bundler replaces the import with a reference.
// The call looks local but crosses the network.

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

export function UsernameForm() {
  const [state, action, isPending] = useActionState(updateUsername, null);

  return (
    <form action={action}>
      <input name="name" disabled={isPending} />
      {state?.error && <p>{state.error}</p>}
      <button type="submit">Update</button>
    </form>
  );
}

// ============================================
// PART 4: THE SERVER REFERENCE SHAPE
// ============================================
// On the client, the imported function is an object.
// It carries an ID and a type symbol.

console.log(updateUsername);
// {
//   $$typeof: Symbol.for("react.server.reference"),
//   $$id: 'updateUsername'
// }

// ============================================
// PART 5: THE PROP-PASSED FORM
// ============================================
// A Server Component can define an inline Server
// Function and pass it as a prop.

// Server Component
export default function Page() {
  async function logVisit() {
    'use server';
    await db.visits.create({ data: { at: new Date() } });
  }

  return <VisitTracker onVisit={logVisit} />;
}

// Client Component
'use client';
function VisitTracker({ onVisit }: { onVisit: () => Promise<void> }) {
  return <button onClick={onVisit}>Log visit</button>;
}

// ============================================
// PART 6: THE FLIGHT ELEMENT FORMAT
// ============================================
// React elements are serialized as arrays.
// The first slot is the element type marker.

// ["$", type, key, props]
// A div with a child:
// ["$","div",null,{"children":["$","p",null,{"children":"Hello"}]}]

// ============================================
// PART 7: THE CLIENT REFERENCE
// ============================================
// Client Components are serialized as import chunks.
// The element references the chunk with $L.

// Import chunk:
// 1:I{"id":"./Counter.js","chunks":["abc"],"name":"Counter"}
// Element:
// ["$","$L1",null,{}]

// ============================================
// PART 8: THE SERIALIZATION BOUNDARY
// ============================================
// Supported: plain objects, arrays, Date, Map, Set.
// Not supported: class instances, React elements,
// non-global symbols, functions.

// This is valid:
await updateUsername(new FormData());

// This is rejected by the protocol:
// await updateUser({ data: new Date() });  // Date is supported
// await updateUser({ element: <div /> });  // React element rejected
// await updateUser({ fn: () => {} });      // function rejected

// ============================================
// PART 9: THE SECURITY CHECK
// ============================================
// The client controls every argument.
// The function must authorize.

export async function deleteAccount(userId: string) {
  'use server';
  const session = await getSession();

  if (session.userId !== userId) {
    throw new Error('Unauthorized');
  }

  await db.users.delete({ where: { id: userId } });
}

// ============================================
// PART 10: THE FULL PICTURE
// ============================================
// A Server Function is a reference, a network call,
// and a serialized result. The wire format is Flight.

export default function App() {
  return (
    <UsernameForm />
  );
}

The ten parts build a Server Function from definition to wire format to security check. The key observation is that the client never holds the function; it holds an ID. The call is a network request, the arguments are serialized according to the Flight protocol, and the return value is serialized back. The security boundary is the argument list, and the function must treat it as untrusted.


Quick Reference

Server Function Forms

FormDirective LocationHow Client Uses It
Inline in Server ComponentFunction bodyPassed as prop
Module-levelTop of fileImported in Client Component
Passed to actionEitherForm submission

Serializable Types

CategorySupportedNot Supported
Primitivesstring, number, bigint, boolean, null, undefined—
SymbolsGlobal (Symbol.for)Local (Symbol())
ObjectsPlain objects, Date, Map, SetClass instances, null-prototype
BinaryTypedArray, ArrayBuffer—
FunctionsServer FunctionsAll other functions
React—JSX, elements

Flight Type Tags

TagMeaning
$DDate
$nBigInt
$SGlobal symbol
$QMap reference
$WSet reference
$LLazy client component
IImport chunk

Server Action vs Server Function

ContextTerm
Passed to action propServer Action
Called inside an actionServer Action
Event handler / programmatic callServer Function
General mechanismServer Function

Best Practices

✅ Do This:

// Mark the module with "use server" at the top.
'use server';                                                        // ✅

// Validate every argument as untrusted input.
if (!name || name.length < 3) return { error: '...' };               // ✅

// Authorize every mutation against the session.
if (session.userId !== resource.ownerId) throw new Error();          // ✅

// Use plain data for arguments.
updateUser({ id: '1', name: 'Alice' });                              // ✅

// Use useActionState for form pending state.
const [state, action, isPending] = useActionState(fn, null);         // ✅

❌ Don’t Do This:

// Don't pass class instances or React elements.
updateUser({ element: <div /> });                                    // ❌

// Don't trust the client's claim of identity.
deleteUser(userIdFromClient); // without session check               // ❌

// Don't use Server Functions for data fetching.
// They are for mutations; frameworks do not cache returns.          // ❌

// Don't define a Server Function outside async.
function syncFn() { 'use server'; }                                  // ❌

// Don't forget the module-level directive for imports.
// A Client Component cannot import an inline function.             // ❌

Common Pitfalls

PitfallWhy It HappensFix
"use server" errorDirective placed after importsMove to top of file or function body
Function not callableNot asyncMark as async
Import failsModule directive missingAdd "use server" at module top
Argument rejectedUnsupported type (class, element)Convert to plain data
Unauthorized mutationNo session checkAuthorize against the authenticated user
Return value not awaitedCalling without awaitawait serverFn()

Real-World Examples

1. Module-Level Server Function

'use server';
export async function save(data: FormData) { ... }

2. Inline Server Function Prop

async function action() { 'use server'; }
return <Button onClick={action} />;

3. Form Action

<form action={updateName}>...</form>

4. useActionState

const [state, action, pending] = useActionState(updateName, null);

5. Authorization Check

if (session.userId !== id) throw new Error('Unauthorized');

6. Server Reference Shape

{$$typeof: Symbol.for("react.server.reference"), $$id: 'fn'}

7. Date Serialization

return { createdAt: new Date() }; // encoded as $D...

8. Map Serialization

return new Map([['a', 1]]); // chunk with $Q reference

9. Client Reference

1:I{"id":"./Counter.js","name":"Counter"}

10. Progressive Enhancement

useActionState(fn, null, '/fallback-url');

Visual

The Server Reference

┌─────────────────────────────────────────────────────────┐
│  SERVER COMPONENT                                       │
│                                                         │
│  async function createNote() { 'use server'; ... }      │
│                                                         │
│  React creates:                                         │
│  {$$typeof: Symbol.for("react.server.reference"),       │
│   $$id: 'createNote'}                                   │
│                                                         │
│  Passes it to Client Component as a prop                │
│         │                                               │
│         ▼                                               │
│  CLIENT COMPONENT                                       │
│                                                         │
│  props.action = {$$typeof: ..., $$id: 'createNote'}     │
│                                                         │
│  action() → network request with $$id and arguments     │
│         │                                               │
│         ▼                                               │
│  SERVER executes createNote, serializes result          │
└─────────────────────────────────────────────────────────┘

The Flight Chunk Format

┌─────────────────────────────────────────────────────────┐
│  Each chunk: <id>:<content>                             │
│                                                         │
│  0:["$","div",null,{"children":"Hello"}]                │
│  │  │                                                  │
│  │  └─ element array                                    │
│  └─ chunk id                                            │
│                                                         │
│  Type tags:                                             │
│  $D → Date    $n → BigInt    $S → Symbol                │
│  $Q → Map     $W → Set       $L → lazy client           │
│                                                         │
│  Binary: <id>:<tag><len>,<bytes>                        │
│  3:o5,Hello  → chunk 3, Uint8Array, len 5, "Hello"      │
└─────────────────────────────────────────────────────────┘

Client Component Import Chunk

┌─────────────────────────────────────────────────────────┐
│  SERVER OUTPUT:                                         │
│                                                         │
│  1:I{"id":"./Counter.js","chunks":["abc"],"name":"Counter"}
│  0:["$","div",null,{"children":[                        │
│       ["$","$L1",null,{}]                               │
│     ]}]                                                 │
│                                                         │
│  Chunk 1 = Import chunk (tag I)                         │
│  Chunk 0 = element tree                                 │
│  "$L1" = lazy reference to chunk 1                      │
│                                                         │
│  CLIENT:                                                │
│  Loads chunk 1, resolves Counter, renders it            │
└─────────────────────────────────────────────────────────┘

The Serialization Boundary

┌─────────────────────────────────────────────────────────┐
│  ARGUMENTS PASS THROUGH THE WIRE                        │
│                                                         │
│  ✅ string, number, bigint, boolean, null, undefined    │
│  ✅ Date, Map, Set, TypedArray, ArrayBuffer             │
│  ✅ Plain objects with serializable values              │
│  ✅ Global symbols (Symbol.for)                         │
│  ✅ Other Server Functions                              │
│                                                         │
│  ❌ Class instances                                     │
│  ❌ Non-global symbols (Symbol())                       │
│  ❌ React elements / JSX                                │
│  ❌ Regular functions                                   │
│  ❌ Event objects                                       │
│                                                         │
│  The client controls every argument.                    │
│  Validate and authorize before mutation.                │
└─────────────────────────────────────────────────────────┘

Summary

ItemValue
Directive"use server"
Function requirementMust be async
Client receivesReference object with $$id
Wire protocolFlight
Element format["$", type, key, props]
Client referenceImport chunk (I), element $L
SerializablePrimitives, Date, Map, Set, plain objects
Not serializableClasses, JSX, functions
SecurityArguments are untrusted; authorize every mutation
Form integrationaction prop, useActionState

Key takeaways:

  • The client holds a reference, not the function. The object {$$typeof: Symbol.for("react.server.reference"), $$id: '...'} is the only thing that crosses the wire besides arguments and results .
  • The directive has two scopes. At the function body, it marks one function. At the module top, it marks every export. Importing requires the module-level form .
  • The Flight protocol is chunked and tagged. Chunk IDs, $-prefixed types, and separate import chunks for Client Components. The format is built for streaming .
  • Client Components are not executed on the server. They are serialized as module references that the client loads and hydrates .
  • Serialization is constrained by what can be reconstructed. Class instances, functions, and JSX are rejected because they cannot be revived on the other side .
  • Security is the caller’s responsibility. Every argument is client-controlled. Validate, escape, and authorize. The function’s existence on the server does not imply every caller is permitted .
  • Server Functions are for mutations, not data fetching. Frameworks typically process one action at a time and do not cache return values. Data fetching belongs to Server Components .
  • Progressive enhancement is built in. Forms submit before JavaScript loads, and useActionState with a permalink handles the pre-hydration submission .
  • The terminology changed. Until September 2024, all Server Functions were called Server Actions. A Server Action is now a Server Function used in an action context .
  • The bundler and framework APIs are not semver-stable. While the React-level Server Function API is stable in React 19, the implementation APIs that bundlers and frameworks use may break between minors. Pin the React version or use Canary .

Remember: A Server Function is a network call disguised as a local function call. The "use server" directive tells the bundler to replace the function with a reference, and the Flight protocol carries the reference, the arguments, and the result across the boundary. The client never sees the function’s code; it sees an ID. The server never trusts the client’s data; it validates and authorizes. The wire format is designed for streaming and for the React element tree, with type tags and chunk IDs that let the client reconstruct the UI and the data. When a Server Function works, the network boundary disappears from the developer’s view. When it fails, the failure is usually in the serialization constraints or in the security assumptions—the two places where the abstraction is thin.


Was this helpful?


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!