Getting Started
Seven steps, one terminal. At the end of step 7 a React component is rendering on a page your Rails app served. After that there is one more section — the ruact:scaffold command from the home page, which turns the same app into a full CRUD resource with a searchable table.
Two ground rules, so you can trust every line below:
- Every code block that names a file is a byte-for-byte excerpt of a runnable example in the ruact repository (
docs/examples/getting-started/). CI boots a Rails app against the gem, renders this guide’s own controller and view, asserts the component arrives with its props, and fails if this page and that example drift apart. - The command blocks name no file, so they are outside that gate. They have their own:
docs/examples/getting-started/scripts/check-commands.mjsholds one canonical sequence and checks every artifact that spells it — this page, the copy-paste command on the home page, the README, the script that runs the same commands from scratch inside a clean container, and the tape that times them. Two blocks on this page are outside both gates, and they are marked where they appear: the layout fragment in step 3, and the list of what the install generator writes.
What you need
Section titled “What you need”| Version | Where that comes from | |
|---|---|---|
| Ruby | >= 3.2 | the gem’s required_ruby_version |
| Rails | 7.0 through 8.x | 7.0, 7.1, 7.2 and 8.0 run in the gem’s CI matrix on every commit; this guide’s own timed run was on 8.1; the gemspec sets no Rails upper bound |
| Node.js | >= 20 | the build only — ruact runs no Node process in production |
Nothing else. No account, no API key, no second terminal tab.
1. Create a Rails app
Section titled “1. Create a Rails app”rails new myapp --skip-javascript && cd myapp--skip-javascript skips Rails’ own importmap setup, because ruact brings its own Vite frontend and the install generator writes the package.json for it. Adding ruact to an app you already have works the same way — skip to step 2.
2. Add the gem
Section titled “2. Add the gem”bundle add ruactbundle add writes the gem "ruact" line into your Gemfile and runs bundle install, in one command.
3. Run the install generator
Section titled “3. Run the install generator”rails generate ruact:installIt writes:
config/initializers/ruact.rb— theRuact.configureblock, withconfig.layout = trueinclude Ruact::Controller, injected into yourApplicationController- the React root and
<%= ruact_js_assets %>, injected intoapp/views/layouts/application.html.erb app/javascript/components/— where your"use client"components livevite.config.jsandpackage.jsonapp/javascript/.ruact/— where ruact regenerates the typed server-functions module (gitignored; you will meet it when you call a Rails action from React)Procfile.devandbin/dev— one command that boots Rails and Vite togetherAGENTS.md— ruact’s conventions, traps and verification commands, for coding agents working in this app
Then it runs npm install for you. Pass --skip-npm if you would rather run your own package manager, and run it before step 6.
The layout edit is the one worth looking at. These three lines go into app/views/layouts/application.html.erb, just before </body>:
<%# ruact: root %><div id="root"></div><%= ruact_js_assets %>That is what makes your layout own the document: your <head>, your stylesheet_link_tag, your fonts and meta tags all still reach a page ruact rendered. The root div and the ruact_js_assets call both have to be there, and config.layout has to be true. Miss either half and you get a page with no styles and no error — which is why the “Did it work?” section below exists.
Here is the package.json the generator just wrote:
{ "name": "myapp", "private": true, "type": "module", "scripts": { "dev": "vite", "build": "vite build" }, "dependencies": { "react": "^19.0.0", "react-dom": "^19.0.0" }, "devDependencies": { "@vitejs/plugin-react": "^4.3.4", "vite": "^6.0.7" }}React 19 and Vite 6, pinned by the generator rather than by this page — a spec renders the gem’s own template and compares it to the block above. You do not need to know either tool to finish this page; if you want them afterwards, react.dev and vite.dev are the sources.
4. Add a page
Section titled “4. Add a page”An ordinary Rails controller and an ordinary ERB view:
rails generate controller Home indexUse the component by name in the view:
<h1><%= @greeting %></h1>
<LikeButton likes={@likes} />And set the two values in the action:
class HomeController < ApplicationController def index @greeting = "Hello from Rails" @likes = 12 endendNo special render call: an action with a matching .html.erb renders through ruact on its own. Three things about that tag, and they are the whole syntax:
- The tag is the component’s export name — not its filename.
LikeButtonbelow is exported asLikeButton, so the tag is<LikeButton />. The capital letter is what marks it as React; lowercase tags stay plain HTML. {}holds Ruby, not JavaScript.likes={@likes}evaluates@likesin the ERB, the way<%= %>would.- The tag is self-closing.
<LikeButton>…</LikeButton>raisesRuact::ChildrenNotSupportedErrorat preprocess time; pass content as a prop instead. The one legitimate pair is<Suspense fallback="…">…</Suspense>.
5. Add the component
Section titled “5. Add the component”Create app/javascript/components/LikeButton.jsx:
"use client"
import { useState } from "react"
export function LikeButton({ likes }) { const [liked, setLiked] = useState(false)
return ( <button onClick={() => setLiked(!liked)}> {liked ? `❤️ ${likes + 1}` : `🤍 ${likes}`} </button> )}"use client" is the only ruact-specific line in that file. It has to be at the top of the module — only comments may come before it — and it is what tells the Vite plugin to scan the file, register the export under its own name, and ship it to the browser. Everything else is a React component you could have written anywhere. There is no registry to edit and no import to add: the plugin scans app/javascript/components/ on its own. More on the directive.
6. Start the dev server
Section titled “6. Start the dev server”bin/devOne command, two processes — Procfile.dev runs bin/rails server and npm run dev. Both are required: Rails serves the page, Vite serves the React and writes the client manifest.
7. Look at it
Section titled “7. Look at it”Open http://localhost:3000/home/index. The heading is Rails; the button is React, and clicking it changes the count without a request. Want it at / instead? Add root "home#index" to config/routes.rb.
That is the whole loop: a Rails action, an ERB template, a component in a directory.
Did it work?
Section titled “Did it work?”If the page looks right, it worked. If something is off, ask the gem before you ask the internet:
bin/rails ruact:doctorIt prints one line per check and exits non-zero on a failure. Three of those lines are worth knowing in advance, because they cover the failures that do not announce themselves:
✓ layout owns the document (React root + ruact_js_assets, config.layout on)⚠ layout is ready but Ruact.config.layout is false✗ layout is missing the React root and/or the ruact_js_assets callThe first is what step 3 should have given you. The other two are the same symptom — a page that renders but has none of your CSS — from the two halves of the same switch, and neither raises an error. Re-running rails generate ruact:install fixes the layout half; the initializer holds the other.
Two failures that are louder. The first is what a bin/rails server on its own gets you on a fresh app — Rails is up, Vite never started, so there is no client manifest to render against:
[ruact] Vite dev server unreachable at http://localhost:5173 and no react-client-manifest.json found at /path/to/myapp/public/react-client-manifest.json — run `bin/dev`.Once Vite has run at least once the manifest file exists, so the same mistake is quieter: the page answers 200, #root stays empty, and the only clue is a 404 on the bundle. ruact:doctor names it either way, which is why it is the first thing to run. And Vite is pinned to port 5173 with strictPort, so a second ruact app on the same machine will not quietly start a second dev server — stop the first one.
The second is a tag with no component behind it, which raises with the name you probably meant:
ruact: Component "LikeButtoon" not found in manifest. Did you mean "LikeButton"? Did you run the Vite build? Run 'npm run build' or start the Vite dev server.That is also what you get when a component’s export name and its tag disagree, or when the export is anonymous (export default function () {}) — the scanner registers capitalised named exports, so an unnamed one never reaches the manifest.
Full check list and the -- --json machine report: ruact:doctor.
Next: a real resource
Section titled “Next: a real resource”The seven steps above are the mechanism. This is what it looks like at the size of an actual feature — and it is the command on the home page.
bin/dev is holding your terminal, so stop it with Ctrl-C first:
rails generate ruact:scaffold Post title:string body:textrails db:migrateThe scaffold hands the model, the migration, the resources route and your test framework’s stubs to Rails’ own resource generator, then adds the ruact layer on top: the controller, the index / show / new / edit views, a PostsQuery with its route, and three components — PostList, PostForm, PostDeleteDialog. It does not run migrations, which is what the second line is for.
Start bin/dev again and open http://localhost:3000/posts: a list that filters as you type, backed by a query that runs in Ruby, and a form that posts to a Rails action. No endpoint was written for either.
The generated components are plain HTML with Rails’ default styling — deliberately, so the scaffold does not decide your design system. If you want shadcn/ui instead, rails generate ruact:install --shadcn writes the two things shadcn’s CLI refuses to start without (a Tailwind entry and a tsconfig.json import alias) and prints two commands it deliberately does not run for you, because they are interactive and hit the network:
npx shadcn@latest init --base radixnpx shadcn@latest add button input textarea switch select label badge table alert-dialog dropdown-menu--base radix is load-bearing: shadcn now defaults to Base UI, and the components ruact:scaffold --shadcn generates import Radix primitives. Then pass --shadcn to the scaffold as well. This is a separate path with its own round-trips, which is why it is not in the seven steps — see Using shadcn/ui components.
What the scaffold generates, option by option: rails generate ruact:scaffold.
Where to go from here
Section titled “Where to go from here”- ERB as Server Components — how a PascalCase tag in ERB becomes a React element, and what happens to your instance variables on the way
- Server functions — calling a Rails action from React, at its real route
"use client"— the rule you just used, in full- AI tools & agents — pasting a generated React file into a Rails app, and the context ruact ships for coding agents
The code, the issues and the examples in this guide: github.com/luizcg/ruact.