Form Handling in Modern Rails: Replacing Rails UJS with Inertia.js useForm
Learn how to replace legacy Rails UJS form submissions with Inertia.js useForm helper, focusing on practical implementation, error handling, and maintaining developer velocity in Rails applications.
The Evolution of Rails Form Handling
Handling forms in Ruby on Rails has historically relied heavily on Rails UJS (Unobtrusive JavaScript). For years, Rails UJS provided a predictable way to submit forms via AJAX, handle remote links, and process server-side validation errors using JavaScript responses. However, as frontend requirements have grown more complex, teams often find themselves reaching for heavy Single Page Application (SPA) frameworks like React or Vue, leading to duplicated routing and complex API layers.
Inertia.js offers a compelling middle ground for modern Rails applications. By bridging the gap between your Rails backend and modern frontend frameworks like React, Vue, or Svelte, Inertia allows you to build SPAs without building an API. When migrating away from Rails UJS, handling form state and validation errors effectively is often the most significant challenge.
This guide explores how to replace legacy Rails UJS form submissions with the Inertia.js useForm helper, focusing on practical implementation, error handling, and maintaining the developer velocity Rails is known for.
Understanding the Limitations of Rails UJS
Rails UJS relies on the data-remote="true" attribute to hijack form submissions and send them via XHR. The server then responds with JavaScript (often a .js.erb file) that executes on the client, updating the DOM or displaying validation errors.
While this approach is elegant for simple applications, it presents several limitations in complex environments:
- State Management: Managing complex client-side state — such as dynamic form fields or multi-step wizards — becomes difficult when the UI is driven by server-rendered JavaScript snippets.
- Testing: Testing
.js.erbresponses often requires slow system tests rather than fast unit tests. - Frontend Tooling: You cannot easily leverage the broad ecosystem of modern frontend components and libraries.
When transitioning to a modern frontend architecture, organizations often struggle to replicate the simplicity of Rails UJS validation handling. Inertia’s useForm utility addresses this gap.
Introducing Inertia.js useForm
The useForm helper in Inertia.js is designed to manage form state, submission, and validation errors effectively. It provides a reactive object that tracks your form fields, processing state, and any errors returned by your Rails controllers.
We will use Svelte for these examples, but the concepts apply equally to React and Vue.
A Basic Form Migration
Consider a traditional Rails UJS form for creating a user:
<%= form_with model: @user, local: false do |form| %>
<div class="field">
<%= form.label :name %>
<%= form.text_field :name %>
<div class="error"><%= @user.errors[:name].join(', ') %></div>
</div>
<%= form.submit "Create User" %>
<% end %>
When upgrading this architecture to Inertia.js, we replace the Rails view with a frontend component utilizing useForm.
<script>
import { useForm } from '@inertiajs/svelte'
const form = useForm({
name: '',
email: ''
})
function submit() {
$form.post('/users')
}
</script>
<form on:submit|preventDefault={submit}>
<div class="field">
<label for="name">Name</label>
<input id="name" type="text" bind:value={$form.name} />
{#if $form.errors.name}
<div class="error">{$form.errors.name}</div>
{/if}
</div>
<button type="submit" disabled={$form.processing}>
Create User
</button>
</form>
In this example, useForm automatically tracks the name and email attributes. The $form.processing boolean is true while the request is in flight, allowing you to disable the submit button and prevent duplicate submissions — a common issue with traditional UJS forms.
Handling Rails Validation Errors
The value of useForm becomes apparent when dealing with validation errors. In a standard Rails controller, you typically render the new or edit template with an unprocessable entity status when validation fails.
With Inertia, the approach remains largely the same, but you return an Inertia response instead:
class UsersController < ApplicationController
def create
@user = User.new(user_params)
if @user.save
redirect_to users_path, notice: "User created successfully."
else
redirect_back fallback_location: new_user_path,
inertia: { errors: @user.errors }
end
end
private
def user_params
params.require(:user).permit(:name, :email)
end
end
Wait, there is a better way. The inertia-rails gem automatically handles validation errors if you follow standard Rails conventions. Instead of manually passing errors, you redirect back. The gem will automatically share the session errors with your frontend.
class UsersController < ApplicationController
def create
@user = User.new(user_params)
if @user.save
redirect_to users_path, notice: "User created successfully."
else
# inertia-rails automatically extracts errors from the session
redirect_back fallback_location: new_user_path
end
end
end
When the redirect occurs, inertia-rails flashes the validation errors to the session. On the subsequent request to render the page, Inertia populates the $form.errors object. Your Svelte component will automatically display the error messages beneath the corresponding inputs, exactly like the legacy Rails UJS approach, but with modern reactive state.
Managing Complex Form State
Legacy applications often rely on complex jQuery or plain JavaScript to handle dynamic form fields. A common requirement is clearing specific fields or resetting the entire form upon successful submission.
The useForm utility provides built-in methods for these scenarios.
Resetting Forms
If you are building a feature like a chat interface or a comment thread, you want to clear the form after the user submits a message. You can chain the onSuccess callback to the post request:
<script>
import { useForm } from '@inertiajs/svelte'
const form = useForm({
body: ''
})
function submit() {
$form.post('/comments', {
preserveScroll: true,
onSuccess: () => form.reset('body'),
})
}
</script>
The preserveScroll: true option is crucial here. It prevents Inertia from scrolling to the top of the page after the request completes, maintaining the user’s context in the comment thread.
Tracking Dirty State
When migrating large, complex settings pages, it is helpful to indicate whether the user has unsaved changes. The $form.isDirty property allows you to conditionally render UI elements based on the form’s state.
{#if $form.isDirty}
<div class="warning">
You have unsaved changes.
</div>
{/if}
This reactive approach provides an improved user experience compared to the manual DOM manipulation required by legacy Rails UJS and jQuery architectures.
Moving Forward with Modern Rails
Replacing Rails UJS with Inertia.js and useForm provides a structured, predictable way to manage form state and validation errors. You retain the rapid development cycle of standard Rails controllers while gaining the power and flexibility of modern frontend components.
When planning a migration away from legacy UJS, focus on updating one form at a time. The interoperability of Inertia allows you to run modern Svelte or React components alongside your existing Rails views, ensuring a safe and gradual transition for your application.
Sponsored by Durable Programming
Need help maintaining or upgrading your Ruby on Rails application? Durable Programming specializes in keeping Rails apps secure, performant, and up-to-date.
Hire Durable Programming