# Inertia Rails Best Practices - Complete Reference This document contains detailed explanations, code examples, and implementation guidance for all Inertia Rails best practices. --- ## 1. Server-Side Setup & Configuration (CRITICAL) These rules establish the foundation for all Inertia functionality. Proper setup ensures reliable operation and maintainability. --- ### setup-01: Use the Rails generator for initial setup **Impact:** CRITICAL - Ensures correct configuration and avoids common setup errors **Problem:** Manual setup can miss important configuration steps, leading to subtle bugs. **Solution:** Use the built-in Rails generator for consistent setup: ```bash # Add the gem bundle add inertia_rails # Run the generator bin/rails generate inertia:install ``` The generator handles: - Vite Rails detection and installation - TypeScript configuration - Frontend framework selection (React, Vue, or Svelte) - Tailwind CSS integration (optional) - Example controller and view files - Application configuration **When manual setup is needed:** ```erb <%= csp_meta_tag %> <%= inertia_ssr_head %> <%= vite_client_tag %> <%= vite_javascript_tag 'application' %> <%= yield %> ``` --- ### setup-02: Configure asset versioning for cache busting **Impact:** CRITICAL - Ensures users receive updated assets after deployments **Problem:** Without version tracking, users may see stale JavaScript after deployments. **Solution:** Configure version in your initializer: ```ruby # config/initializers/inertia_rails.rb InertiaRails.configure do |config| # Using ViteRuby digest (recommended) config.version = -> { ViteRuby.digest } # Or using a custom version string # config.version = Rails.application.config.assets_version # Or using Git commit hash # config.version = -> { `git rev-parse HEAD`.strip } end ``` **How it works:** When the version changes, Inertia triggers a full page visit instead of an XHR request, ensuring fresh assets are loaded. --- ### setup-03: Set up proper layout inheritance **Impact:** HIGH - Enables flexible layout management across controllers **Problem:** All Inertia pages using the same layout limits design flexibility. **Solution:** Configure default layout and override per-controller: ```ruby # config/initializers/inertia_rails.rb InertiaRails.configure do |config| config.layout = 'application' # default end # app/controllers/admin/base_controller.rb class Admin::BaseController < ApplicationController layout 'admin' end # Or use inertia_config for controller-specific settings class MarketingController < ApplicationController inertia_config(layout: 'marketing') end ``` --- ### setup-04: Configure flash keys appropriately **Impact:** MEDIUM - Ensures proper flash message delivery to frontend **Problem:** Custom flash keys may not be passed to the frontend by default. **Solution:** Configure allowed flash keys: ```ruby # config/initializers/inertia_rails.rb InertiaRails.configure do |config| # Default: %i[notice alert] config.flash_keys = %i[notice alert success error warning info] end ``` For custom flash data beyond allowlisted keys: ```ruby # Controller flash.inertia[:custom_data] = { message: 'Success!', type: 'toast' } # Or for current request only flash.inertia.now[:custom_data] = { message: 'Success!' } ``` --- ### setup-05: Use environment variables for configuration **Impact:** MEDIUM - Enables deployment flexibility without code changes **Problem:** Hardcoded configuration limits deployment options. **Solution:** Inertia Rails supports `INERTIA_` prefixed environment variables: ```bash # .env or deployment config INERTIA_SSR_ENABLED=true INERTIA_SSR_URL=http://localhost:13714 INERTIA_ENCRYPT_HISTORY=true INERTIA_DEEP_MERGE_SHARED_DATA=true ``` Boolean values must be exactly `"true"` or `"false"` (case-sensitive). --- ### setup-06: Set up default render behavior thoughtfully **Impact:** MEDIUM - Reduces boilerplate while maintaining explicitness **Problem:** Overly implicit rendering can make code harder to understand. ```ruby # config/initializers/inertia_rails.rb InertiaRails.configure do |config| # Enable convention-based rendering config.default_render = true # Customize component path resolution config.component_path_resolver = ->(path:, action:) do "#{path.camelize}/#{action.camelize}" end end ``` **Incorrect - implicit rendering without clear conventions:** ```ruby class UsersController < ApplicationController def show @user = User.find(params[:id]) # What component gets rendered? Unclear. end end ``` **Correct - explicit rendering:** ```ruby class UsersController < ApplicationController def show user = User.find(params[:id]) render inertia: { user: user.as_json(only: [:id, :name, :email]) } end end ``` --- ## 2. Props & Data Management (CRITICAL) Proper props management is crucial for performance and security. These rules can provide 2-5× performance improvements. --- ### props-01: Return only necessary data in props **Impact:** CRITICAL - Reduces payload size, improves security, prevents data leaks **Problem:** Returning entire ActiveRecord objects exposes unnecessary data and bloats responses. **Incorrect:** ```ruby def show render inertia: { user: User.find(params[:id]) } # Exposes all columns including password_digest, tokens, etc. end ``` **Correct:** ```ruby def show user = User.find(params[:id]) render inertia: { user: user.as_json(only: [:id, :name, :email, :avatar_url]) } end # Or with associations def show user = User.find(params[:id]) render inertia: { user: user.as_json( only: [:id, :name, :email], include: { posts: { only: [:id, :title, :published_at] } } ) } end ``` **Note:** Some browsers limit history state size (Firefox: 16 MiB). Keep props minimal. --- ### props-02: Use shared data for global props **Impact:** HIGH - Reduces duplication, centralizes common data **Problem:** Repeating the same props in every controller action. **Incorrect:** ```ruby class UsersController < ApplicationController def index render inertia: { current_user: current_user.as_json(only: [:id, :name]), app_name: Rails.configuration.app_name, users: User.all } end def show render inertia: { current_user: current_user.as_json(only: [:id, :name]), app_name: Rails.configuration.app_name, user: User.find(params[:id]) } end end ``` **Correct:** ```ruby # app/controllers/application_controller.rb class ApplicationController < ActionController::Base # Static data - evaluated once inertia_share app_name: Rails.configuration.app_name # Dynamic data - evaluated per request inertia_share do if user_signed_in? { auth: { user: current_user.as_json(only: [:id, :name, :email, :avatar_url]) } } else { auth: { user: nil } } end end # Lambda for lazy evaluation inertia_share notifications_count: -> { current_user&.unread_notifications_count } end # Controller only needs page-specific data class UsersController < ApplicationController def index render inertia: { users: User.all.as_json(only: [:id, :name]) } end end ``` --- ### props-03: Leverage lazy evaluation with lambdas **Impact:** HIGH - Prevents unnecessary database queries **Problem:** Eagerly evaluated props execute even when not needed. **Incorrect:** ```ruby inertia_share do { # This query runs on EVERY request, even if not used recent_posts: Post.recent.limit(5).as_json } end ``` **Correct:** ```ruby inertia_share do { # Only evaluated when actually accessed recent_posts: -> { Post.recent.limit(5).as_json } } end ``` --- ### props-04: Use deferred props for non-critical data **Impact:** HIGH - Improves perceived performance by loading page faster **Problem:** Expensive data queries block initial page render. **Incorrect:** ```ruby def dashboard render inertia: { user: current_user, # These block initial render analytics: Analytics.expensive_query, recommendations: Recommendations.compute_for(current_user) } end ``` **Correct:** ```ruby def dashboard render inertia: { user: current_user.as_json(only: [:id, :name]), # Loaded after initial render analytics: InertiaRails.defer { Analytics.expensive_query }, # Group related deferred props recommendations: InertiaRails.defer(group: 'suggestions') { Recommendations.compute_for(current_user) }, similar_users: InertiaRails.defer(group: 'suggestions') { User.similar_to(current_user) } } end ``` **Frontend handling:** ```vue ``` --- ### props-05: Implement partial reloads correctly **Impact:** MEDIUM-HIGH - Reduces data transfer on page refreshes **Problem:** Reloading entire page data when only some props changed. **Incorrect:** ```javascript // Reloads all props router.reload() ``` **Correct:** ```javascript // Only reload specific props router.reload({ only: ['users'] }) // Exclude specific props router.reload({ except: ['analytics'] }) // In Link component Refresh Users ``` **Server-side optimization:** ```ruby def index render inertia: { # Always included users: User.all.as_json(only: [:id, :name]), # Only included when explicitly requested statistics: InertiaRails.optional { compute_statistics }, # Always included, even in partial reloads csrf_token: InertiaRails.always { form_authenticity_token } } end ``` --- ### props-06: Never expose sensitive data in props **Impact:** CRITICAL - Security vulnerability prevention **Problem:** Props are visible in browser DevTools and can be cached in history. **Incorrect:** ```ruby def show render inertia: { user: User.find(params[:id]) # Exposes: password_digest, reset_token, api_keys, etc. } end ``` **Correct:** ```ruby def show user = User.find(params[:id]) render inertia: { user: { id: user.id, name: user.name, email: user.email, # Only public attributes } } end # Or use a serializer/presenter def show user = User.find(params[:id]) render inertia: { user: UserPresenter.new(user).as_json } end ``` --- ### props-07: Use proper serialization with as_json **Impact:** MEDIUM - Consistent data formatting, prevents accidental exposure **Correct patterns:** ```ruby # Simple whitelist user.as_json(only: [:id, :name, :email]) # With associations user.as_json( only: [:id, :name], include: { posts: { only: [:id, :title] }, profile: { only: [:bio, :avatar_url] } } ) # With computed methods user.as_json( only: [:id, :name], methods: [:full_name, :avatar_url] ) # Exclude specific fields user.as_json(except: [:password_digest, :remember_token]) ``` --- ### props-08: Implement deep merge when appropriate **Impact:** MEDIUM - Proper handling of nested shared data **Problem:** Shallow merge overwrites nested objects unexpectedly. ```ruby # config/initializers/inertia_rails.rb InertiaRails.configure do |config| config.deep_merge_shared_data = true end # Or per-request render inertia: { nested: { key: 'value' } }, deep_merge: true ``` --- ### props-09: Use once props for stable data **Impact:** MEDIUM - Reduces redundant data transfer **Problem:** Stable data resent on every navigation. **Correct:** ```ruby # Resolved once and remembered across navigations inertia_share do { app_config: InertiaRails.once { AppConfig.to_json }, feature_flags: InertiaRails.once { FeatureFlags.for_user(current_user) } } end ``` --- ## 3. Forms & Validation (HIGH) Forms are central to most applications. These patterns ensure good user experience and data integrity. --- ### forms-01: Use useForm helper for complex forms **Impact:** HIGH - Provides state management, validation, and submission handling **Problem:** Managing form state manually is error-prone and verbose. **Correct (React):** ```jsx import { useForm } from '@inertiajs/react' export default function CreateUser() { const { data, setData, post, processing, errors, progress, reset } = useForm({ name: '', email: '', password: '', avatar: null, }) function submit(e) { e.preventDefault() post('/users', { onSuccess: () => reset('password'), preserveScroll: true, }) } return (
setData('name', e.target.value)} /> {errors.name &&
{errors.name}
} setData('email', e.target.value)} /> {errors.email &&
{errors.email}
} setData('password', e.target.value)} /> setData('avatar', e.target.files[0])} /> {progress && } ) } ``` **Key useForm features:** - `form.processing` - Prevents double submission - `form.progress` - File upload progress - `form.errors` - Validation errors from server - `form.isDirty` - Detects unsaved changes - `form.reset()` - Reset specific or all fields - `form.clearErrors()` - Clear validation errors --- ### forms-02: Use Form component for simple forms **Impact:** MEDIUM - Declarative syntax for straightforward forms ```jsx import { Form } from '@inertiajs/react' export default function CreateUser() { return (
{({ errors, processing }) => ( <> {errors.name &&
{errors.name}
} )}
) } ``` --- ### forms-03: Handle validation errors properly **Impact:** HIGH - Clear user feedback on validation failures **Server-side:** ```ruby class UsersController < ApplicationController def create user = User.new(user_params) if user.save redirect_to users_url, notice: 'User created successfully' else redirect_to new_user_url, inertia: { errors: user.errors } end end end ``` **Client-side:** ```jsx
setData('email', e.target.value)} /> {errors.email && {errors.email}}
``` --- ### forms-04: Implement error bags for multiple forms **Impact:** MEDIUM - Prevents error collision on pages with multiple forms **Problem:** Two forms with same field names share error states. **Solution:** ```javascript // Form 1 - Create user const createForm = useForm({ email: '' }) createForm.post('/users', { errorBag: 'createUser' }) // Form 2 - Invite user const inviteForm = useForm({ email: '' }) inviteForm.post('/invitations', { errorBag: 'inviteUser' }) ``` Access errors at `page.props.errors.createUser` and `page.props.errors.inviteUser`. --- ### forms-05: Use redirect pattern after form submission **Impact:** HIGH - Follows PRG pattern, prevents duplicate submissions **Incorrect:** ```ruby def create @user = User.create(user_params) render inertia: { user: @user } # Can cause duplicate on refresh end ``` **Correct:** ```ruby def create user = User.new(user_params) if user.save redirect_to user_url(user), notice: 'User created!' else redirect_to new_user_url, inertia: { errors: user.errors } end end ``` --- ### forms-06: Handle file uploads correctly **Impact:** MEDIUM - Proper multipart form handling ```ruby # Controller def create user = User.new(user_params) user.avatar.attach(params[:avatar]) if params[:avatar] # ... end ``` ```jsx const { data, setData, post, progress } = useForm({ name: '', avatar: null, }) function submit(e) { e.preventDefault() post('/users') // Automatically uses FormData when files present } return (
setData('avatar', e.target.files[0])} /> {progress && ( {progress.percentage}% )}
) ``` **For PUT/PATCH with files (method spoofing):** ```javascript form.post(`/users/${user.id}`, { _method: 'put', // Rails handles this via Rack::MethodOverride }) ``` --- ### forms-07: Preserve form state on validation errors **Impact:** HIGH - Users don't lose their input on errors Inertia automatically preserves component state for POST, PUT, PATCH, DELETE requests. The form data persists; you only need to display errors. ```ruby # Server - redirect back with errors def create user = User.new(user_params) unless user.save redirect_to new_user_url, inertia: { errors: user.errors } end end ``` --- ### forms-08: Use dotted notation for nested data **Impact:** MEDIUM - Clean handling of complex form structures ```jsx
{({ errors }) => ( <>