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.


What is Flight?

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.

Row types

PrefixMeaningExample
(none, just JSON)Model row — a JSON value0:{"title":"Hello"}
IImport row — registers a client module1:I["/Btn.jsx","Button",["/Btn.jsx"]]
EError row — encodes a serialized error2:E{"message":"Not found"}

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


How Ruby values map to Flight rows

Ruby valueFlight output
nil0:null
true / false0:true / 0:false
420:42
"hello"0:"hello"
"$danger"0:"$$danger"$ prefix escaped to $$
[1, "a", true]0:[1,"a",true]
{ debug: true }0:{"debug":true}
ClientReferenceImport row + model row referencing $L{id}

Client component reference example

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

Why import rows come first

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.


Debugging Flight output

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

bash
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.


Adding support for a new Ruby type

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

Released under the MIT License.