Serializable State

TL;DR
  • Serialize plain data only
  • Supported: primitives, arrays, plain objects, Dates, Map/Set, TypedArrays, URL/SearchParams, BigInt
    • References and cycles are preserved
  • Avoid: user-defined functions, non-built-in class instances, DOM, closures

State passed from server to client must be serialized into HTML. Keeping state plain and deterministic ensures reliable hydration and enables compiler optimizations.

Serializable Data

State is embedded into HTML during server rendering. The serializer supports the following plain data types:

  • Primitives: null, boolean, number, string, bigint
  • Arrays and plain objects with serializable values
  • Dates
  • Map, Set
  • Typed arrays and ArrayBuffer/DataView
  • URL and URLSearchParams
  • Additional built-in JS and Browser objects

Nested values must also be serializable.

<let/user={
  id: 12,
  name: "Marko",
  tags: ["admin", "editor"],
  created: "2024-04-15",
}>

<const/created=new Date(user.created)>

<let/ids=new Set([user.id])>
let/user={
  id: 12,
  name: "Marko",
  tags: ["admin", "editor"],
  created: "2024-04-15",
}

const/created=new Date(user.created)

let/ids=new Set([user.id])
<let/user={
  id: 12,
  name: "Marko",
  tags: ["admin", "editor"],
  created: "2024-04-15",
}>

<const/created=new Date(user.created)>

<let/ids=new Set([user.id])>
let/user={
  id: 12,
  name: "Marko",
  tags: ["admin", "editor"],
  created: "2024-04-15",
}

const/created=new Date(user.created)

let/ids=new Set([user.id])

Unserializable Data

Some values cannot be embedded into HTML in a stable, deterministic way. These should not generally be stored in state:

  • Closures over JS instance variables
  • Class instances (except built-ins explicitly supported by the runtime)
  • DOM nodes and elements
Note
<Most
  functions
  and
  closures
  in
  Marko
  _are_
  serializable.
  Static
  variables
  and
  Marko
  state
  can
  be
  used
  safely./>
<let/handler=null>
<const/onSecondClick() {
  // serializable!
}>

<button onClick() {
  handler?.();
  handler = onSecondClick;
}/>
Most
  ,functions
  ,and
  ,closures
  ,in
  ,Marko
  ,_are_
  ,serializable.
  ,Static
  ,variables
  ,and
  ,Marko
  ,state
  ,can
  ,be
  ,used
  ,safely.
let/handler=null
const/onSecondClick() {
  // serializable!
}

button onClick() {
  handler?.();
  handler = onSecondClick;
}
<Most
  functions
  and
  closures
  in
  Marko
  _are_
  serializable.
  Static
  variables
  and
  Marko
  state
  can
  be
  used
  safely./>
<let/handler=null>
<const/onSecondClick() {
  // serializable!
}>

<button onClick() {
  handler?.();
  handler = onSecondClick;
}/>
Most
  ,functions
  ,and
  ,closures
  ,in
  ,Marko
  ,_are_
  ,serializable.
  ,Static
  ,variables
  ,and
  ,Marko
  ,state
  ,can
  ,be
  ,used
  ,safely.
let/handler=null
const/onSecondClick() {
  // serializable!
}

button onClick() {
  handler?.();
  handler = onSecondClick;
}

Instead, keep plain data in state and construct functions, class instances, or DOM references at usage sites (for example, in event handlers or server actions).

// ❌ BAD: function in state
<let/state={ save: () => doThing() }>
// ❌ BAD: custom class instance in state
<let/state=new Cart()>
// ❌ BAD: DOM nodes in state
<let/state={ el: document.body }>
// ❌ BAD: function in state
let/state={ save: () => doThing() }
// ❌ BAD: custom class instance in state
let/state=new Cart()
// ❌ BAD: DOM nodes in state
let/state={ el: document.body }
// ❌ BAD: function in state
<let/state={ save: () => doThing() }>
// ❌ BAD: custom class instance in state
<let/state=new Cart()>
// ❌ BAD: DOM nodes in state
<let/state={ el: document.body }>
// ❌ BAD: function in state
let/state={ save: () => doThing() }
// ❌ BAD: custom class instance in state
let/state=new Cart()
// ❌ BAD: DOM nodes in state
let/state={ el: document.body }

Further Reading


Contributors

Helpful? You can thank these awesome people! You can also edit this doc if you see any issues or want to improve it.