ruactapp/views/home/index.html.erbdocsgithub200 OKGet started

ruact 0.0.22 · React Server Components for Rails

Write Rails.
Ship React.

A React component, called from an ERB template.

No JSON layer to write by hand. No second application to keep in step. No Node process in production.

Capitalized means React. Lowercase stays HTML. The tag is the component's export name, and { } holds Ruby, evaluated the way <%= %> would.

One directive. "use client" at the top of the file is all the component needs to know about ruact.

index.html.erbapp/views/home
1<h1><%= @greeting %></h1>
2
3<LikeButton likes={@likes} />
LikeButton.jsxapp/javascript/components
1"use client"
2import { useState } from "react"
3
4export function LikeButton({ likes }) {
5  const [liked, setLiked] = useState(false)
6  return (
7    <button onClick={() => setLiked(!liked)}>
8      {liked ? `❤️ ${likes + 1}` : `🤍 ${likes}`}
9    </button>
10  )
11}
rendered by React

What it is: your ERB view, sent to the browser in React's own wire format, the one React Server Components use, and rendered there by React.

  • No server-rendered HTML for the React tree. Pages a crawler must read stay in ERB.
  • A ruact page is one React tree, in ruact's layout by default. Your other pages keep their layout, Turbo and Stimulus.
  • Suspense does not wait on slow data yet. The whole template renders before the page is sent.

install

Four commands and one line, and it is your own app.

Not a sandbox and not a starter template. A Rails application you made a minute ago, with ruact added to it.

37.0smachine time to the page answering, cold npm cache

zshtimed run · 2026-10-22 · ruact 0.0.22
  • rails new myapp --skip-javascript13.48s
  • bundle add ruact4.09s
  • rails generate ruact:install14.07s
  • rails generate controller Home index2.40s
  • include Ruact::Controller in HomeControllerby hand

total, through to the page answering37.0s

The install writes the package.json, the Procfile.dev and bin/dev, then runs npm install for you. It changes none of your controllers: a page renders through ruact once its controller has that one line. Node builds the bundle and is needed nowhere else.

render

The Ruby value crossed. Nobody wrote a serializer.

@likes was an integer in a controller. It arrived as a prop. There is no endpoint behind this, no fetch on mount, and no client cache to invalidate: the data is inlined in the page, so React renders immediately.

A model crosses as the fields you list with ruact_props :id, :title. In production one without the list raises instead of crossing; in development it crosses as as_json, every attribute, and the log names it.

home_controller.rbapp/controllers
1class HomeController < ApplicationController
2  include Ruact::Controller
3
4  def index
5    @greeting = "Hello from Rails"
6    @likes = 12
7  end
8end
localhost:3000/home/index

Hello from Rails

Rendered by React. likes came from @likes.

The component above is React running in this page, not a screenshot of one. Click it, or tap it.

So what happens when you get it wrong?

break

Break it on purpose.

Misspell the component, or leave its tag open. This editor checks the template for those two mistakes the way ruact does, with ruact's own messages. It does not run Ruby.

index.html.erbeditable

or type in the box

responseGET /home/index

Hello from Rails

Rendered by React. likes came from @likes.

The template compiled and the component rendered.

It stopped server side, while the template was being processed. Nothing reached the browser, and the message names the component, the file, the line and the fix. That is the whole seam: it is checkable, and it says what it wants.

start

Start where
you already are.

Add it to the Rails application you have open right now. It coexists with the ERB you have already written, view by view: a page renders through ruact once its controller has include Ruact::Controller, and every other page stays as it is.