React Server Components for Ruby on Rails. Write ERB, pass Ruby values as props, hydrate only where you need interactivity — no rewrites, no route duplication, no Node.js in production.
<PostCard post={@post} author={@author} />
1 2 3 4 5 6 7 8 9 10 11 12 13 14
"use client" export function PostCard({ post, author }) { const [liked, setLiked] = useState(false) return ( <article> <h1>{post.title}</h1> <p>by {author.name}</p> <button onClick={() => setLiked(!liked)}> {liked ? "Liked" : "Like"} </button> </article> ) }
Adopting React in a Rails app usually means paying one of two taxes. The full SPA: duplicate your routes, build an API layer, solve auth twice, maintain a second app. Or sprinkles: keep Rails but give up the React component model, its ecosystem, and typed data flow.
ruact removes the trade-off — Rails stays the single source of truth for routes, auth, and data; React is simply how your views render.
<PostCard post={@post} />
1:I["PostCard.jsx","PostCard"] 0:["$","$L1",null,{"post":{"title":"Hello"}}]
<article>…</article>
A PascalCase tag in ERB — <PostCard post={@post} /> — becomes a React element. Props are native Ruby values.
The component tree ships as a React Flight payload, produced by a pure-Ruby implementation of the wire format. No Node.js server anywhere in production.
The browser renders the tree and hydrates only the components marked "use client". Navigation, redirects, and scroll restoration are derived from your Rails routes.
Include one module and your controller actions become typed TypeScript functions. The route verb decides exposure, the route table generates the types, and your existing before_action auth and CSRF protection keep working — because it's still just Rails handling the request.
1 2 3 4 5 6 7 8 9
class PostsController < ApplicationController include Ruact::Server def create @post = Post.create!(post_params) redirect_to @post end end
1 2 3 4 5
import { createPost } from "@/.ruact/server-functions" export function PostForm() { return <form action={createPost}>…</form> }
Client-side navigation, redirects, and typed accessors are all derived from rails routes — nothing is declared twice.
A full server-side implementation of React's wire format. Node.js is a build tool, not a runtime dependency.
Server-rendered by default; opt into interactivity per component, exactly like RSC intends.
Query params are reflected from your method signatures into TypeScript — no annotations, no drift.
Three components mounting the same query issue one GET.
Models declare exactly which attributes cross the wire. Nothing leaks by default.
Typos get file, line, and a "did you mean?" — at template-compile time, not in the browser console.
Controllers, actions, views, models. If you know Rails MVC, you already know ruact.
The wire protocol serializes Ruby values out; it never reconstructs objects from client bytes. That single invariant places ruact outside the entire class of RSC deserialization vulnerabilities — by construction, not by patching. A built-in tripwire in ruact:doctor guards the invariant on every run. Record references cross the wire as signed, purpose-scoped, expiring tokens — never raw IDs.
Read the security model →The installer owns the whole setup — Vite config, React entry point, bin/dev, the initializer — and ruact:doctor diagnoses your app when something's off. When you typo a component name, this is the error, at compile time:
ruact: Component "LikeButtoon" not found in manifest. Did you mean "posts/_like_button"? Did you run the Vite build? Run 'npm run build' or start the Vite dev server.
Ruby 3.2+ · Rails 7.0–8.x · React 19 · Node 20+ (build only)