docs / api reference
Ruact.configure
Ruact.configure — every configuration option, its default and when to change it.
Configure ruact via Ruact.configure in an initializer.
The install generator creates config/initializers/ruact.rb:
Ruact.configure do |config| # config.manifest_path = Rails.root.join("public", "react-client-manifest.json") # config.strict_serialization = true # config.suspense_timeout = 5.0 # config.vite_dev_server = "http://localhost:5173"endAll settings have sensible defaults. You only need to set the ones you want to override.
Configuration Options
Section titled “Configuration Options”manifest_path
Section titled “manifest_path”Type: String, Pathname, or nil
Default: Rails.root.join("public/react-client-manifest.json")
Path to the react-client-manifest.json generated by the Vite plugin. Override this if you move the manifest to a non-default location.
config.manifest_path = Rails.root.join("public", "rsc-manifest.json")The manifest is loaded once at boot time (via Rails’ to_prepare hook) and cached for the lifetime of the process. It is not re-read on every request.
strict_serialization
Section titled “strict_serialization”Type: Boolean
Default: false in development/test, true in production
When true, objects without an explicit ruact_props declaration raise Ruact::SerializationError instead of falling back to as_json. This prevents accidental attribute exposure in production.
# Explicitly opt in to strict mode in all environmentsconfig.strict_serialization = trueRecommended: enable strict mode in production. Disable in development for faster prototyping.
suspense_timeout
Section titled “suspense_timeout”Type: Float
Default: 5.0 (seconds)
How long, in seconds of real time, one <Suspense>’s content may run in an action that streams. Past it, that boundary shows an error (its fallback stays) and the next one runs. A delay counts inside it.
config.suspense_timeout = 10.0It is a backstop. With PostgreSQL, a query the content is still running at the limit is cancelled, so its connection is free again — except a query load_async runs, which runs to its end. Bound queries with the database’s statement timeout, set below this one: it is the real limit. In an action that does not stream, a <Suspense>’s content runs before the page is sent, and this setting does not apply.
vite_dev_server
Section titled “vite_dev_server”Type: String
Default: "http://localhost:5173"
Base URL of the Vite dev server. ruact uses this to inject the correct <script> tags into the HTML shell during development.
config.vite_dev_server = "http://localhost:3036"In production, Vite is not used — production assets are served from public/assets/.
signed_global_id_default_purpose
Section titled “signed_global_id_default_purpose”Type: Symbol | String | nil
Default: nil
App-wide default for: purpose used by Ruact.signed_global_id /
Ruact.locate_signed
when a call omits for:. A purpose scopes a signed reference to one use-site. Left
nil, ruact requires an explicit for: per call and raises rather than sign an
unscoped token.
config.signed_global_id_default_purpose = :ruact_refsigned_global_id_default_expires_in
Section titled “signed_global_id_default_expires_in”Type: ActiveSupport::Duration | nil
Default: nil
App-wide default token lifetime used when a call omits expires_in:. Must be an
ActiveSupport::Duration (e.g. 15.minutes). Left nil, ruact requires an
explicit expires_in: per call and raises rather than mint a non-expiring token;
pass an explicit expires_in: nil at the call site to deliberately opt into a
non-expiring token.
config.signed_global_id_default_expires_in = 15.minutesserver_function_hidden_ivars
Section titled “server_function_hidden_ivars”Type: Array<String>
Default: ["current_user", "current_ability", "pundit"] (Devise, CanCanCan, Pundit)
Instance-variable names, without the @, that a server function’s JSON never returns, even when the action set them. They are the memos that authentication and authorization libraries fill as a side effect of a call, such as current_user or authorize. Names starting with _ are always hidden.
Ruact.configure do |config| # keep the defaults and hide an app's own memo too config.server_function_hidden_ivars += ["current_account"]endlayout
Section titled “layout”Type: "ruact" | true | false | String
Default: false — rails generate ruact:install writes "ruact"
Which document a ruact page’s HTML response renders into. The Flight response of an in-app navigation is never affected.
"ruact"— the layout ruact ships. Links your client components’ CSS and the stylesheets inlayout_stylesheets, and edits none of your layouts.rails generate ruact:layoutcopies it into your app when you want to own it; your copy takes over with no other setting. Like any named layout, it applies to every ruact page whatever the controller declares withlayout— includinglayout false. Its<title>is your app’s name on every page; per-page titles need your own copy.true— your controller’s normal Rails layout, which then needs<%= ruact_head_assets %>in<head>and<%= ruact_js_assets %>next to a<div id="root"></div>. The install prints those lines; it does not write them.- another String — that named layout, with the same requirements.
false— ruact’s minimal built-in shell, with none of your stylesheets.
config.layout = "ruact"layout_stylesheets
Section titled “layout_stylesheets”Type: Array<Symbol | String>
Default: [:app]
The stylesheets ruact’s layout links after your client components’ CSS, passed as-is to stylesheet_link_tag. [:app] is what rails new links, and under Propshaft it means every stylesheet you have, a Tailwind build included. The install writes ["application"] in an app on Sprockets. [] links none.
config.layout_stylesheets = ["application", "print"]Which CSS reaches the page, and who links it
Section titled “Which CSS reaches the page, and who links it”A ruact app has three sources of CSS travelling through two pipelines. Knowing which is which is the difference between styling that works everywhere and styling that works in development and vanishes in production.
| Source | Built by | Reaches the page via |
|---|---|---|
app/assets/stylesheets/application.css |
Propshaft (or Sprockets) | stylesheet_link_tag — named in config.layout_stylesheets |
A Tailwind entry (the --shadcn path) |
Tailwind CLI → Propshaft | the same stylesheet_link_tag :app, which picks up app/assets/builds/ |
CSS imported by a "use client" component |
Vite | ruact_head_assets |
The third row is the one worth knowing about. Vite is an asset bundler, not a JavaScript bundler: a client component that imports a stylesheet — its own, or one an npm package ships — produces a separate CSS file. ruact_head_assets is what links it.
Who calls these. By default nobody but ruact: rails generate ruact:install sets config.layout = "ruact", so ruact pages render through a layout that ships with the gem, and its <head> includes:
<head> <%= csrf_meta_tags %> <%= csp_meta_tag %> <%= ruact_head_assets %> <%= stylesheet_link_tag(*Ruact.config.layout_stylesheets) %></head>None of your layouts is edited. config.layout_stylesheets is [:app] under Propshaft 0.9 and later; the install writes ["application"] on Sprockets or an older Propshaft, where :app would name a file that does not exist, and [] when your app has neither. :app (and :all) must be the whole list — Propshaft ignores anything next to them. That layout brings your stylesheets and nothing else of your own <head> — no favicons, fonts or analytics, and none of your JavaScript. When a page needs more, rails generate ruact:layout copies the layout into app/views/layouts/ruact.html.erb: it is yours from then on, and it takes over from ruact’s without any setting. The alternative is config.layout = true, which renders through your application layout; that one needs the two helpers added by hand, and the install prints the lines.
The order is deliberate. ruact’s line goes first so your own CSS loads afterwards. Order only decides ties — specificity, !important and cascade layers all outrank it — but ties are the common case, and losing them by default is what makes a third-party stylesheet feel like it took over your page. If you wire your own layout, keep that order; rails ruact:doctor fails if the build emits component CSS the layout rendering your pages never links.
With the dev server running the helper emits nothing, on purpose: Vite is already injecting that CSS through JavaScript, and linking the file on disk would serve whatever the last build left there. In development without the dev server it falls back to the built manifest, which is what the script tag does too.
If you use TypeScript, add vite/client to your tsconfig.json so a stylesheet import has declarations to resolve against:
{ "compilerOptions": { "types": ["vite/client"] } }rails generate ruact:install --shadcn writes that for you, because shadcn’s CLI refuses to start without a tsconfig.json at all. Outside that flag ruact does not write one: your TypeScript setup is yours, and one line is cheaper than a generated file that might overwrite it.
Accessing configuration at runtime
Section titled “Accessing configuration at runtime”# Read the current configRuact.config.manifest_pathRuact.config.strict_serialization
# Access the loaded manifest (post-boot)Ruact.manifest # => Ruact::ClientManifest instanceFull example
Section titled “Full example”Ruact.configure do |config| # Custom manifest location config.manifest_path = Rails.root.join("public", "react-client-manifest.json")
# Strict mode in production — prevents accidental data leaks config.strict_serialization = Rails.env.production?
# 10-second timeout for slow data fetches config.suspense_timeout = 10.0
# Custom Vite port (if you changed it in vite.config.js) config.vite_dev_server = "http://localhost:5173"end