Skip to content

Flight Wire Format

React Server Components use a streaming protocol called Flight to transfer the component tree from server to browser. You do not need to understand Flight to use ruact — but knowing the basics helps when debugging unexpected output or adding support for new Ruby types.


Flight is a line-based text format where each line is a row encoding one piece of the React element tree:

1:I["/LikeButton.jsx","LikeButton",["/LikeButton.jsx"]]
0:["$","$L1",null,{"postId":42}]
  • Row 0 is always the root — the top-level React element tree for the page.
  • Row 1, 2, … are supporting data: import registrations, async chunks, or referenced sub-trees.

Prefix Meaning Example
(none, just JSON) Model row — a JSON value 0:{"title":"Hello"}
I Import row — registers a client module 1:I["/Btn.jsx","Button",["/Btn.jsx"]]
E Error row — encodes a serialized error 2:E{"message":"Not found"}

Row IDs are hexadecimal: 0, 1, 2, …, 9, a, b, …, f, 10, 11, …


Ruby value Flight output
nil 0:null
true / false 0:true / 0:false
42 0:42
"hello" 0:"hello"
"$danger" 0:"$$danger"$ prefix escaped to $$
[1, "a", true] 0:[1,"a",true]
{ debug: true } 0:{"debug":true}
ClientReference Import row + model row referencing $L{id}

When your ERB includes <LikeButton postId={@post.id} />, ruact emits:

1:I["/LikeButton.jsx","LikeButton",["/LikeButton.jsx"]]
0:["$","$L1",null,{"postId":42}]

Breaking it down:

  • Row 1 (import): registers /LikeButton.jsx with export name "LikeButton", bundle chunks ["/LikeButton.jsx"]
  • Row 0 (root): a React element — ["$", type, key, props]:
    • "$" — the React element marker
    • "$L1" — the type: a lazy reference to import row 1 (the LikeButton module)
    • null — the element key
    • {"postId":42} — the props

The browser processes rows as they arrive. A model row that references "$L1" cannot be rendered until import row 1 is registered. ruact always emits import rows before the model rows that reference them.


The simplest way to see the raw Flight output for any page:

Terminal window
curl -H "Accept: text/x-component" http://localhost:3000/posts/1

The text/x-component Accept header tells ruact to return only the Flight payload (no HTML shell). This is what the browser sends during client-side navigation.


If you need ruact to serialize a custom Ruby type, follow the fixture-first workflow described in CONTRIBUTING.md:

  1. Write a .txt fixture with the expected wire output
  2. Write a failing spec using match_flight_fixture
  3. Add a case/when branch in gem/lib/ruact/flight/serializer.rb
  4. Verify the spec passes with no regressions