ERB as Server Components
ruact lets you write your UI in ERB — exactly as you know from Rails — and automatically serializes it as a React element tree. No JSX on the server, no separate component files for server logic.
How it works
When you use a PascalCase tag in an ERB template, ruact treats it as a React element:
<!-- app/views/posts/index.html.erb -->
<h1>Posts</h1>
<% @posts.each do |post| %>
<PostCard post={post} />
<% end %>The ERB preprocessor converts <PostCard post={post} /> into a call that creates a React element with the PostCard type and { post: post } as props. The Flight serializer then encodes the entire tree as a wire format payload.
The browser receives this payload and renders it as a live React application — including full interactivity for any "use client" components nested inside.
Tag rules
| ERB syntax | React equivalent | Notes |
|---|---|---|
<PostCard /> | <PostCard /> | Self-closing, no props |
<PostCard post={@post} /> | <PostCard post={post} /> | Ruby expression as prop value |
<PostCard title={"Hello"} /> | <PostCard title="Hello" /> | String props use braces too |
<Suspense fallback="Loading..."> | <Suspense fallback={...}> | The one paired tag; fallback is a quoted string |
Prop values are Ruby expressions
Everything inside {...} is evaluated as Ruby:
<PostCard
post={@post}
canEdit={current_user.admin?}
comments={@post.comments.limit(5)}
/>Rails helpers, ActiveRecord associations, Ruby methods — all work as expected. The values are serialized to JSON by the Flight serializer before reaching the browser.
Always use braces. Unbraced attribute values are not supported on component tags — <PostCard title="Hello" /> is silently dropped today (the prop never reaches the component). Write title={"Hello"}: the braces hold a Ruby expression, and a quoted Ruby string is one.
No children content
Component tags do not support child content between tags in the current version:
<!-- NOT supported yet -->
<Card>
<p>Some content</p>
</Card>
<!-- Use props instead -->
<Card content={"Some content"} />Today children between component tags fail silently — they never reach the component. The one exception is the built-in <Suspense> tag (below), which is paired.
Suspense and streaming
<Suspense> — the one paired component tag — defers part of the page:
<Suspense fallback="Loading comments...">
<CommentsList postId={@post.id} />
</Suspense>Be precise about what this does today:
- Evaluation is eager and in-request. The ERB inside the boundary — including any data work its props do — runs on the server, during the request, before anything is sent. Suspense does not make data loading asynchronous.
- What is deferred is the emission. The already-evaluated content is emitted as a separate, later chunk of the Flight payload; the client shows
fallbackuntil that chunk arrives. - Progressive delivery requires
ActionController::Live. With it, the fallback flushes first and the deferred chunk streams afterwards — seeRuact::Controller§ Streaming. Without it, the whole payload (fallback and deferred chunk) is buffered and sent at once, and the boundary resolves immediately in the browser. - An optional per-boundary
delayattribute (seconds) postpones the chunk's emission when streaming, bounded byRuact.config.suspense_timeout(exceeding the timeout emits an error row for that boundary instead).
What gets serialized
When your controller calls ruact_render, ruact:
- Evaluates the ERB template
- Collects all PascalCase component references (and their props)
- Serializes the result as a Flight wire format payload
- Sends the payload (or an HTML shell wrapping it, for first-page loads)
Only the values you pass as props reach the browser. If you pass @post as a prop, only the serialized form of @post is sent — not the entire ActiveRecord object. Use Ruact::Serializable or as_json to control exactly which fields are exposed.
HTML elements
Plain HTML elements in ERB are also converted to React elements. ruact uses Nokogiri to parse the HTML output and converts it to a React element tree:
<div class="post">
<h2><%= @post.title %></h2>
<p><%= @post.body %></p>
</div>Becomes the React element tree ["$", "div", null, { className: "post", children: [...] }].
Note on
classvsclassName: ruact automatically convertsclass→className,for→htmlFor, and handlesvalue/checked→defaultValue/defaultCheckedfor uncontrolled form inputs.