Serializable State

Marko seamlessly picks up where the server left off when it comes to events, scripts and client side updates through state. In order to do this Marko will attempt to serialize as little data as possible from the server to the client.

Most standard data types can be serialized, including:

  • Primitives: null, boolean, number, string, bigint
  • Arrays and plain objects with serializable values
  • Dates and regular expressions
  • Map, Set
  • Typed arrays and ArrayBuffer/DataView
  • URL and URLSearchParams
  • Headers, FormData, Request, Response
  • Built-in error types, including AggregateError
  • Intl formatters and Temporal values
  • Well-known and registered symbols
  • Generators, async generators, and ReadableStream
  • Additional built-in JS and Browser objects

State reaches the client automatically when something there depends on it. $global does not: it is server data until a property is named in $global.serializedGlobals, which is what makes request-scoped values such as a locale readable after hydration.

Shared References

The payload is JavaScript, not JSON, so a value reached twice is written once and referenced everywhere else. Identity carries over with it: objects that are the same on the server are the same object in the browser, and cyclic structures are restored as cycles.

Consider a comment list where several comments share one author record:

<let/muted=null>
<ul>
  <for|comment| of=input.comments>
    <li class=(comment.author === muted && "muted")>
      ${comment.text}
      <button onClick() {
        muted = comment.author;
      }>mute</button>
    </li>
  </for>
</ul>
<let/muted=null>
<ul>
  <for|comment| of=input.comments>
    <li class=(comment.author === muted && "muted")>
      ${comment.text}
      <button onClick() { muted = comment.author }>mute</button>
    </li>
  </for>
</ul>
let/muted=null
ul
  for|comment| of=input.comments
    li class=(comment.author === muted && "muted")
      -- ${comment.text}
      button onClick() {
        muted = comment.author;
      } -- mute
let/muted=null
ul
  for|comment| of=input.comments
    li class=(comment.author === muted && "muted")
      -- ${comment.text}
      button onClick() { muted = comment.author } -- mute

Every comment by one author points at a single serialized record, so muting one of them dims the rest without comparing ids. The same holds across streaming flushes, where a later chunk refers back to a value an earlier chunk already sent.

Unserializable Data

Some values cannot be serialized. When these values are encountered the Marko runtime will provide a helpful message to locate the relevant code.

Examples of unserializable data include:

  • Closures (top level functions are fine!)
  • Functions that come from arbitrary javascript code, such as a .js or .ts module
  • Class instances (except built-ins explicitly supported by the runtime)
  • DOM nodes and elements
Note

Most functions and closures are serializable.

<let/handler=null>
<const/onSecondClick() {
  ; // serializable!
}>
<button onClick() {
  handler?.();
  handler = onSecondClick;
}/>
<let/handler=null>
<const/onSecondClick() { 
  // serializable!
}>

<button onClick() { handler?.(); handler = onSecondClick }/>
let/handler=null
const/onSecondClick() {
  ; // serializable!
}
button onClick() {
  handler?.();
  handler = onSecondClick;
}
let/handler=null
const/onSecondClick() { 
  // serializable!
}

button onClick() { handler?.(); handler = onSecondClick }
<!-- ❌ BAD: custom class instance in state-->
<let/state=(new Cart())>
<!-- ❌ BAD: DOM nodes in state-->
<let/state={
  el: document.body
}>
// ❌ BAD: custom class instance in state
<let/state=new Cart()>

// ❌ BAD: DOM nodes in state
<let/state={ el: document.body }>
<!-- ❌ BAD: custom class instance in state-->
let/state=(new Cart())
<!-- ❌ BAD: DOM nodes in state-->
let/state={
  el: document.body
}
// ❌ 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.