# ruact > React Server Components for Rails — "Write Rails. Ship React." ruact renders > ERB templates as React Server Components (the React Flight wire format) with > no Node server; Rails routes are the single source of truth for pages, the > client router, and every server function. This file is the compact LLM reference for ruact, kept in sync manually with the AGENTS.md that `rails generate ruact:install` emits into every app. ruact is NOT Inertia, react-rails, or Next.js — do not transplant their patterns. ## Mental model - A page is a normal Rails controller action rendering a normal `.html.erb`. - Interactive components live in `app/javascript/components/` as `"use client"` files, mounted from ERB with a PascalCase self-closing tag: ``. - Mutations: `include Ruact::Server` in a controller makes its routed non-GET actions (`POST`/`PATCH`/`PUT`/`DELETE`) callable from React — the action IS the function, at its real route; GET page actions are untouched. The JS runtime forwards the CSRF token; include the module AFTER `protect_from_forgery`. - Reads: public methods on `Ruact::Query` subclasses in `app/queries/`, mounted in routes with `ruact_queries CatalogQuery` (one GET route per method, default prefix `/q`), consumed via the `useQuery` hook (`{ data, loading, error }`). Params come from the method's keyword arguments; only `string | number | boolean | null` values are accepted. Queries run the host controller's callback chain (auth) before instantiating. - ruact generates a typed TS module from the route table; React imports server functions from it: `import { createPost } from "@/.ruact/server-functions"`. - Ground truth: `app/javascript/.ruact/server-functions.ts` (gitignored) is the authoritative accessor list — READ IT instead of simulating the codegen. Regenerate with `bin/rails ruact:server_functions:generate`. ## Five traps 1. Component tags take no children — client component tags are self-closing ONLY (``, never `...`). Children (a matching closing tag) fail LOUDLY with a `PreprocessorError` at preprocess time; pass content as a prop. Sole exception: the built-in `...` pair. 2. `{}` props are Ruby, not JavaScript — `label={@post.title}` evaluates Ruby in the ERB. No JS ternaries, no `{...spread}`, no JSX. Unbraced string props are not supported. 3. One action, two response shapes — a `Ruact::Server` non-GET action answers JSON (its instance variables, or `204`, or `{"$redirect": path}`) when the request's `Accept` header is exactly `application/json` — what every generated-accessor call sends, including `
`. Any other request shape renders normally (Flight for client-side navigation, HTML otherwise). The caller picks the shape, not the controller body. 4. `ruact_errors` requires fall-through — call `ruact_errors(@record)` on the failed-save branch and let the action end there; ruact injects `errors: { attribute: [messages] }` into the JSON. An explicit `render` on that branch opts out. With `redirect_to`, errors ride flash to the next render. 5. Accessor names are derived, not declared — `posts#create` → `createPost`, `posts#publish_all` → `publishAllPosts`, query `search_users` → `searchUsers`. Collisions fail loudly at boot; break an action tie with `ruact_function_name :action, as: "otherName"`, a query tie by renaming the query method. Never guess a name — read the generated file. ## Safety - Serialization is an allowlist: only the fields you name cross to the client. `include Ruact::Serializable` + `ruact_props :id, :title` is that allowlist — on a PORO OR directly on an ActiveRecord model. AR defines its attribute readers lazily, so for an AR model the loud check fires on the first serialize (not at class-load) — a valid model boots, a typo still raises a clean `ArgumentError`. The manual wire hash (`{ id: post.id, title: post.title }`) is the equivalent for a no-model / partial shape. A domain object with no allowlist warns and falls back to `as_json` in dev; production (`strict_serialization`) raises, as does an `as_json` returning `self`. - Record references: mint scoped signed tokens instead of trusting raw ids — `Ruact.signed_global_id(record, for: :purpose, expires_in: 1.hour)` and `Ruact.locate_signed(token, for: :purpose)` (tampered/expired/wrong purpose → 400). Purpose is always required; expiry must be explicit (`nil` only as a deliberate non-expiring choice). ## Verify - `bin/rails ruact:doctor` — install/config health; exits 1 on failure. Append `-- --json` for the same checks as a machine-readable report. - `bin/rails ruact:routes -- --json` — the accessor/route table (each accessor's name, kind, verb, path, params) as JSON, from the same route table codegen reads; bare `ruact:routes` prints a compact human table. - Both `--json` outputs are EXPERIMENTAL — the shape may change; gate on their `schema_version` field (currently `0`), do not treat it as a stable contract. - `bin/rails ruact:server_functions:generate` — regenerates the TS module; exits 1 on a naming collision or invalid name. - `bin/dev` — boots Rails AND Vite (both required; Vite writes the client manifest). - If the app has TypeScript tooling configured, `npx tsc --noEmit` type-checks call sites against the generated accessor types (a fresh install does not ship a tsconfig). - In request specs, `require "ruact/testing"` then assert a page rendered a component: `expect(response).to have_ruact_component("PostList").with_props(including("posts"))` — no browser. Function-call/query responses are JSON — use `JSON.parse(response.body)`, not `have_ruact_component`. ## Docs - [Getting started](https://ruact.dev/docs/getting-started): install ruact and render a first client component - [ERB as Server Components](https://ruact.dev/docs/concepts/erb-as-server-components): the core rendering model - [Server functions](https://ruact.dev/docs/api/server-actions): the verb rule, JSON/Flight negotiation, queries, `useQuery`, validation errors, naming - [Ruact::Controller](https://ruact.dev/docs/api/controller): page rendering, redirects, streaming - [Ruact::Serializable](https://ruact.dev/docs/api/serializable): `ruact_props` and the serialization allowlist - [Configuration](https://ruact.dev/docs/api/configuration): `Ruact.configure` keys - [ruact:doctor](https://ruact.dev/docs/api/ruact-doctor): the health-check task - ["use client" directive](https://ruact.dev/docs/concepts/use-client): what makes a component interactive ## Optional - [Client-side navigation](https://ruact.dev/docs/concepts/navigation): the route-derived client router - [Flight wire format](https://ruact.dev/docs/concepts/flight-wire-format): protocol internals - [Full CRUD with scaffold](https://ruact.dev/docs/guides/full-crud-scaffold): `rails generate ruact:scaffold` end to end - [Testing](https://ruact.dev/docs/guides/testing): assert renders server-side with `have_ruact_component`; JSON for function calls and queries - [Progressive migration](https://ruact.dev/docs/guides/progressive-migration): adopting ruact in an existing app - [Changelog](https://ruact.dev/docs/changelog): release history