Skip to content

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:

erb
<!-- 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 syntaxReact equivalentNotes
<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:

erb
<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:

erb
<!-- 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:

erb
<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 fallback until that chunk arrives.
  • Progressive delivery requires ActionController::Live. With it, the fallback flushes first and the deferred chunk streams afterwards — see Ruact::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 delay attribute (seconds) postpones the chunk's emission when streaming, bounded by Ruact.config.suspense_timeout (exceeding the timeout emits an error row for that boundary instead).

What gets serialized

When your controller calls ruact_render, ruact:

  1. Evaluates the ERB template
  2. Collects all PascalCase component references (and their props)
  3. Serializes the result as a Flight wire format payload
  4. 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:

erb
<div class="post">
  <h2><%= @post.title %></h2>
  <p><%= @post.body %></p>
</div>

Becomes the React element tree ["$", "div", null, { className: "post", children: [...] }].

Note on class vs className: ruact automatically converts classclassName, forhtmlFor, and handles value/checkeddefaultValue/defaultChecked for uncontrolled form inputs.

Released under the MIT License.