Skip to content

Client-Side Navigation

ruact provides client-side navigation out of the box — no React Router config, no duplicate route definitions. Your Rails routes are the single source of truth.


When the RSC client boots in the browser, it intercepts clicks on <a> elements that point to the same origin. Instead of a full page reload, it:

  1. Sends GET /path with Accept: text/x-component
  2. Rails detects the header and calls ruact_render (which returns a Flight payload, no HTML shell)
  3. The client parses the Flight payload and renders the new component tree in place

The URL updates via history.pushState — the browser back/forward buttons work normally.

Regular links just work:

erb
<%= link_to "View Post", post_path(@post) %>
<!-- or -->
<a href="/posts/<%= @post.id %>">View Post</a>

No special components needed. No <Link> wrapper. Plain HTML anchors.


State preservation during navigation

The RSC client preserves client component state across navigations when the component tree structure remains the same. For example, if a <SearchInput> appears on every page at the same position, its state (search query, focus) is preserved when the user navigates.

State is reset when:

  • The component is unmounted (it no longer appears in the new page's tree)
  • The component's key changes

Forms (POST, PUT, PATCH, DELETE)

ruact intercepts form submissions for forms that target the same origin. The request is sent as a standard Rails form submission (with CSRF token), and the response is treated as a new Flight payload:

erb
<%= form_with url: posts_path, method: :post do |f| %>
  <%= f.text_field :title %>
  <%= f.submit "Create" %>
<% end %>

In your controller:

ruby
def create
  @post = Post.create!(post_params)
  ruact_render   # ← renders the updated UI as a Flight payload
end

Redirect after POST

For the standard Post-Redirect-Get pattern:

ruby
def create
  @post = Post.create!(post_params)
  redirect_to post_path(@post)
end

When ruact detects a redirect response to a Flight request, it serializes the redirect as a special Flight row:

0:{"redirectUrl":"/posts/1","redirectType":"push"}

The client follows the redirect client-side — no full page reload, no flash of unstyled content.


When NOT to use ruact_render

Some controller actions should remain plain Rails responses:

  • File downloads (send_file, send_data)
  • JSON APIs consumed by third parties
  • SSE streams, webhooks

In these cases, use regular render or respond_to blocks. ruact does not interfere with actions that do not call ruact_render.


Turbo and Hotwire coexistence

ruact and Turbo/Hotwire serve different use cases and can coexist in the same app. Use ruact for RSC-powered pages and Turbo for pages that remain traditional Rails HTML. The Ruact::Controller concern only activates for actions that call ruact_render.

Released under the MIT License.