docs / api reference
Ruact::Controller
Ruact::Controller — the concern that adds RSC rendering to any Rails controller.
Ruact::Controller is an ActiveSupport::Concern that adds RSC rendering capability to any Rails controller.
Include it in the controllers whose pages should render through ruact:
class PostsController < ApplicationController include Ruact::ControllerendEvery action of that controller with a corresponding .html.erb template then renders through ruact; your other controllers stay ordinary Rails. rails generate ruact:install changes no controller (island mode), and rails generate ruact:scaffold includes the concern in the controllers it generates.
To make every page of the app a ruact page instead, include it in ApplicationController — which is what rails generate ruact:install --app does (whole-app mode).
Only some actions: ruact_pages
Section titled “Only some actions: ruact_pages”A page is a whole controller or the actions it declares:
class PostsController < ApplicationController include Ruact::Controller ruact_pages only: %i[show preview] # or: except: %i[index]
def index; end # ordinary Rails, with its own layout
def show; end # ruact
def preview ruact_render(template: "posts/show") # ruact — declared, so it counts end # without a template of its ownendAn action listed in only: is a page even without a template of its own, which is how an action that renders another template is treated as a ruact page by the router. A name in only: that is neither a method nor a template of the controller declaring it raises Ruact::ConfigurationError — a typo does not quietly make the page plain Rails. (except: is not checked: naming a missing action there excludes nothing. A declaration on a base controller is a policy for its subclasses and is not checked against each of them.)
The declaration covers forms too. A POST/PATCH/DELETE from the router to an action outside it is submitted by the browser, like any Rails form: its response is a full page load, and it shows validation errors the Rails way — render :new, status: :unprocessable_entity, which still renders new through ruact when new is a page (explicit render) — while ruact’s ruact_errors + redirect_to redirect-back is for actions inside the declaration. List an action in only: to keep its forms inside the client router.
Automatic rendering
Section titled “Automatic rendering”Once the concern is included, the default_render hook activates RSC rendering automatically when:
- A matching
.html.erbtemplate exists for the current action - The request is either an HTML request (
Accept: text/html) or an RSC request (Accept: text/x-component)
For all other formats (JSON, XML, etc.), the default Rails rendering is used unchanged.
Explicit render of a page
Section titled “Explicit render of a page”A render whose template is a ruact page of the same controller goes through ruact, with its status: and locals: — so the Rails idiom for a failed save works on a ruact page:
def create @post = Post.new(post_params) if @post.save redirect_to @post else ruact_errors(@post) render :new, status: :unprocessable_entity # new.html.erb, rendered by ruact, 422 endendrender :new, render "new", render action: :new and render template: "posts/new" (from PostsController) all count — when that .html.erb is the template Rails would pick for the request. The format Rails negotiated decides: a .json URL, a format.json branch of respond_to or an Accept that prefers JSON render the JSON template as always, so a scaffold’s format.json { render :show, status: :created, location: @post } is untouched. “A ruact page” is what default_render uses: the action is a page (every action, or the ones in ruact_pages) and its .html.erb is in this controller’s own app/views folder. The client router renders the 422 answer in place and keeps the form’s URL.
Everything else is Rails’ own: render json:, plain:, html:, partial: and every other renderer; a template of another folder; an action outside ruact_pages; and a request that accepts neither HTML nor Flight (a server-function call asking for JSON). status:, locals:, location:, variant:, locale:, formats: and assigns: apply as Rails applies them; layout: and content_type: do not — a ruact page renders into config.layout, as HTML or Flight, like every ruact page. Any other option — a gem’s, like wicked_pdf’s pdf:, or a key Rails ignores, like alert: (the flash goes in flash.now) — leaves the render to Rails, and if the page holds a client component the error names that option. To render another folder’s template through ruact, call ruact_render with template: and status:; a client component in a template Rails renders on its own raises an error naming both ways.
respond_to works in both requests a ruact page gets. A browser load and a client-router request (navigation or form submit) both run the format.html branch, and its render, status or redirect is the answer, so the controller rails generate scaffold writes works as it is. A .json URL and an Accept: application/json request run format.json. A respond_to with no format.html answers a router request with 406, and the error names the missing branch; link to such an action with data-ruact="false" so the browser loads it instead.
ruact_render
Section titled “ruact_render”Renders the RSC view for the current action.
def ruact_render(template: nil, locals: {}, status: nil)Parameters
Section titled “Parameters”| Parameter | Type | Default | Description |
|---|---|---|---|
template |
String, nil |
nil |
Logical template name (e.g. "posts/custom"). When nil, uses the current action’s default template. |
locals |
Hash |
{} |
Local variables to pass to the template. |
status |
Symbol, Integer, nil |
nil |
The response status, as render status: takes it (:unprocessable_entity, 422…), in the HTML document and the Flight payload alike. nil keeps the response’s status (200). |
Behaviour
Section titled “Behaviour”- RSC request (
Accept: text/x-componentorRuact-Request: 1header): responds with atext/x-componentFlight payload. - HTML request: responds with an HTML document carrying the Flight payload in a
<script>block. - In an action that streams, both are sent as the page renders: the shell first, then each
<Suspense>’s content when it is ready.
Examples
Section titled “Examples”# Use the action's default template (most common)def index @posts = Post.all ruact_renderend
# Render a different templatedef create @post = Post.new(post_params) if @post.save redirect_to @post else ruact_render template: "posts/new", status: :unprocessable_entity endend
# Pass localsdef show @post = Post.find(params[:id]) ruact_render locals: { highlight: params[:highlight] }endredirect_to (overridden for RSC)
Section titled “redirect_to (overridden for RSC)”The concern overrides Rails’ redirect_to for RSC requests. Instead of issuing a 302 HTTP redirect, it emits a Flight redirect row:
0:{"redirectUrl":"/posts/1","redirectType":"push"}The client-side router intercepts this row and navigates without an extra HTTP round-trip.
Behaviour:
- RSC request + same-origin URL: emits Flight redirect row
- RSC request + cross-origin URL: falls back to standard
302redirect - Non-RSC request: falls back to standard Rails
redirect_to
def create @post = Post.create!(post_params) redirect_to post_path(@post) # ← handled by RSC client when called from JS navigationendStreaming
Section titled “Streaming”ruact_streaming declares the actions whose page is sent as it renders: the shell and every <Suspense> fallback first, then each <Suspense>’s content, run after the shell is sent, when it is ready — on the first load and on a navigation. See Suspense and streaming for what runs later and what does not.
class PostsController < ApplicationController include Ruact::Controller ruact_streaming only: :show
def show @post = Post.find(params[:id]) @comments = @post.comments.load_async # runs while the shell is sent endend| Form | Streams |
|---|---|
ruact_streaming |
every ruact page of the controller (an action with no template of its own, a JSON one, does not) |
ruact_streaming only: :show |
the listed actions; a name that is not an action fails loudly |
ruact_streaming except: :index |
every ruact page but the listed ones |
only: and except: cannot be combined. The declaration is inherited, and a subclass can redeclare it; one line in ApplicationController makes every page of the app stream.
The declared actions run under ActionController::Live, chosen per action before the action runs: the controller’s other actions respond as before — a buffered response, its ETag, on the request’s thread. An endpoint built with PostsController.action(:show) is included. A controller that includes ActionController::Live itself, and declares nothing, streams every page.
An action that renders a <Suspense> and does not stream renders its content in place, before anything is sent; in development the log says, once per response, which line to add.
A declared action that gets a request over HTTP/1.0 — nginx’s default to its upstream, and Rack::Test in request specs — cannot stream: Live gives it an ordinary response and the page is sent whole. The app logs a warning the first time, in every environment, naming the action and proxy_http_version 1.1;. See Deploying an app that streams.
Detecting RSC requests
Section titled “Detecting RSC requests”The concern exposes a private helper you can use in before_actions or other callbacks:
before_action :require_authentication, unless: :ruact_request?An RSC request is one that includes Accept: text/x-component or the header Ruact-Request: 1.