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.
1<h1><%= @greeting %></h1> 2 3<LikeButton likes={@likes} />
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}
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
- $ rails new myapp --skip-javascript13.48s
- $ bundle add ruact4.09s
- $ rails generate ruact:install14.07s
- $ rails generate controller Home index2.40s
include Ruact::ControllerinHomeControllerby 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.
1class HomeController < ApplicationController 2 include Ruact::Controller 3 4 def index 5 @greeting = "Hello from Rails" 6 @likes = 12 7 end 8end
Hello from Rails
Rendered by React. likes came from @likes.
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.
or type in the box
Hello from Rails
Rendered by React. likes came from @likes.
The template compiled and the component rendered.
Raised while the template was compiled, before any HTML was sent. The original error is Ruact::UnknownComponentError, carried as the cause: the tag names no component the build knows. The spec that pins this message.
Raised while the template was compiled, before any HTML was sent. The original error is Ruact::PreprocessorError, carried as the cause: a component attribute takes {ruby}, "text", or no value at all.
Raised while the template was compiled, before any HTML was sent. The original error is Ruact::ComponentTagError, carried as the cause: every component tag closes, with /> or with its closing tag.
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.