React in Ruby on Rails Cheat Sheet
Overview
This guide covers the low-level mechanics of integrating React components into Rails views, passing data between Rails and React, and the basic architectural patterns.
Basic Setup (Quick Reference)
Installation
# Add react-rails gem
bundle add react-rails
# Install React via Webpack/Shakapacker
rails webpacker:install:react
# OR for newer Rails with importmap
rails importmap:install
rails turbo:installCreating React Components
Component File Location
app/javascript/components/MyComponent.jsx
Basic Component Structure
jsx// app/javascript/components/HelloWorld.jsx
import React from 'react';
const HelloWorld = ({ name, message }) => {
return (
<div className="hello-world">
<h1>Hello, {name}!</h1>
<p>{message}</p>
</div>
);
};
export default HelloWorld;Component with State
jsx// app/javascript/components/Counter.jsx
import React, { useState } from 'react';
const Counter = ({ initialCount = 0 }) => {
const [count, setCount] = useState(initialCount);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setCount(count - 1)}>Decrement</button>
</div>
);
};
export default Counter;Registering Components
With react-rails gem
// app/javascript/components/index.js
import HelloWorld from './HelloWorld';
import Counter from './Counter';
import UserProfile from './UserProfile';
// Register components for use in Rails views
const components = {
HelloWorld,
Counter,
UserProfile
};
export default components;Entry Point Setup
// app/javascript/application.js
import React from 'react';
import { createRoot } from 'react-dom/client';
import components from './components';
// Auto-mount components on page load
document.addEventListener('DOMContentLoaded', () => {
const mountPoints = document.querySelectorAll('[data-react-component]');
mountPoints.forEach((mountPoint) => {
const componentName = mountPoint.dataset.reactComponent;
const props = JSON.parse(mountPoint.dataset.reactProps || '{}');
const Component = components[componentName];
if (Component) {
const root = createRoot(mountPoint);
root.render(<Component {...props} />);
}
});
});Using Components in Rails Views
Method 1: Using react-rails Helper
<!-- app/views/pages/index.html.erb -->
<!-- Simple component with no props -->
<%= react_component("HelloWorld") %>
<!-- Component with props -->
<%= react_component("HelloWorld", {
name: "John",
message: "Welcome to Rails with React!"
}) %>
<!-- Component with data from controller -->
<%= react_component("UserProfile", {
user: @user.as_json,
editable: current_user == @user
}) %>
<!-- Component with nested data -->
<%= react_component("Dashboard", {
stats: {
users: @users_count,
posts: @posts_count,
comments: @comments_count
},
recentActivity: @recent_activities.as_json
}) %>Method 2: Manual DIV with data attributes
<!-- app/views/pages/show.html.erb -->
<div
data-react-component="Counter"
data-react-props='<%= { initialCount: 10 }.to_json %>'
></div>
<!-- With controller data -->
<div
data-react-component="UserProfile"
data-react-props='<%= @user.to_json %>'
></div>Method 3: Inline JSON Script (for complex data)
<!-- app/views/posts/show.html.erb -->
<div id="post-detail-root"></div>
<script type="application/json" id="post-data">
<%= raw @post.to_json(include: [:author, :comments]) %>
</script>
<script>
document.addEventListener('DOMContentLoaded', () => {
const postData = JSON.parse(
document.getElementById('post-data').textContent
);
// Mount component with data
// (Your JS bundle handles this)
});
</script>Controller Setup
Basic Controller Pattern
# app/controllers/posts_controller.rb
class PostsController < ApplicationController
def index
# Fetch data for React component
@posts = Post.includes(:author)
.order(created_at: :desc)
.limit(20)
# Optional: prepare data structure for React
@posts_data = @posts.map do |post|
{
id: post.id,
title: post.title,
excerpt: post.excerpt,
author: {
name: post.author.name,
avatar_url: post.author.avatar_url
},
created_at: post.created_at.iso8601
}
end
end
def show
@post = Post.includes(:author, comments: :user).find(params[:id])
# Serialize for React consumption
@post_json = @post.as_json(
include: {
author: { only: [:id, :name, :avatar_url] },
comments: {
include: {
user: { only: [:id, :name] }
}
}
}
)
end
endController with API Endpoint (Hybrid Approach)
# app/controllers/api/posts_controller.rb
module Api
class PostsController < ApplicationController
# For React component that fetches data after mount
def index
@posts = Post.includes(:author).all
respond_to do |format|
format.html # Renders normal view
format.json { render json: @posts }
end
end
def show
@post = Post.find(params[:id])
render json: @post.to_json(include: [:author, :comments])
end
end
endModel Serialization
Simple Serialization
# app/models/user.rb
class User < ApplicationRecord
has_many :posts
# Define what gets sent to React
def as_json(options = {})
super(options.merge(
only: [:id, :name, :email, :avatar_url],
methods: [:full_name]
))
end
def full_name
"#{first_name} #{last_name}"
end
endUsing Serializers (Better for complex data)
# app/serializers/user_serializer.rb
class UserSerializer
def initialize(user)
@user = user
end
def as_json
{
id: @user.id,
name: @user.name,
email: @user.email,
avatar_url: @user.avatar_url,
posts_count: @user.posts.count,
joined_at: @user.created_at.iso8601
}
end
end
# In controller:
@user_data = UserSerializer.new(@user).as_jsonPassing Data Patterns
Pattern 1: Server-Rendered Props (Initial Load)
<!-- View -->
<%= react_component("TodoList", {
todos: @todos.as_json,
user_id: current_user.id,
can_edit: policy(@todos).edit?
}) %>// Component receives all data upfront
const TodoList = ({ todos, userId, canEdit }) => {
const [items, setItems] = useState(todos);
// Component works with initial data
};Pattern 2: Fetch After Mount (SPA-like)
<!-- View passes minimal props -->
<%= react_component("TodoList", {
user_id: current_user.id,
api_endpoint: api_todos_path
}) %>// Component fetches its own data
const TodoList = ({ userId, apiEndpoint }) => {
const [todos, setTodos] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(apiEndpoint)
.then(res => res.json())
.then(data => {
setTodos(data);
setLoading(false);
});
}, [apiEndpoint]);
if (loading) return <div>Loading...</div>;
return <div>{/* Render todos */}</div>;
};Pattern 3: Hybrid (Initial + Updates)
<%= react_component("PostDetail", {
post: @post.as_json,
comments_url: post_comments_path(@post)
}) %>const PostDetail = ({ post: initialPost, commentsUrl }) => {
const [post, setPost] = useState(initialPost);
const [comments, setComments] = useState(initialPost.comments);
const loadMoreComments = () => {
fetch(`${commentsUrl}?offset=${comments.length}`)
.then(res => res.json())
.then(newComments => setComments([...comments, ...newComments]));
};
};Common Patterns & Gotchas
Environment Variables
<!-- Pass Rails env vars to React -->
<%= react_component("App", {
api_key: ENV['API_KEY'],
environment: Rails.env,
base_url: root_url
}) %>Date/Time Handling
# Controller - always use ISO8601 for dates
@event_data = {
starts_at: @event.starts_at.iso8601,
ends_at: @event.ends_at.iso8601
}// Component
const EventDetail = ({ startsAt, endsAt }) => {
const startDate = new Date(startsAt);
return <div>{startDate.toLocaleDateString()}</div>;
};Turbo/Turbolinks Compatibility
// If using Turbo, listen for turbo:load instead of DOMContentLoaded
document.addEventListener('turbo:load', () => {
// Mount React components
});
// Cleanup on navigation
document.addEventListener('turbo:before-render', () => {
// Unmount React components if needed
});Asset Pipeline URLs
<%= react_component("ImageGallery", {
images: @images.map { |img|
{
id: img.id,
url: image_url(img.file),
thumbnail_url: image_url(img.thumbnail)
}
}
}) %>File Structure Example
app/
├── controllers/
│ ├── posts_controller.rb # Traditional Rails controller
│ └── api/
│ └── posts_controller.rb # API endpoints for React
├── models/
│ └── post.rb # Model with as_json customization
├── views/
│ ├── posts/
│ │ ├── index.html.erb # Contains react_component calls
│ │ └── show.html.erb
│ └── layouts/
│ └── application.html.erb # Includes React bundle
├── javascript/
│ ├── application.js # Entry point, mounts components
│ ├── components/
│ │ ├── index.js # Exports all components
│ │ ├── PostList.jsx
│ │ ├── PostDetail.jsx
│ │ └── PostForm.jsx
│ └── utils/
│ ├── csrf.js # CSRF helpers
│ └── api.js # API wrapper functions
└── serializers/ # Optional
└── post_serializer.rbSummary
- Data flows one direction: Controller → View → React Props
- Serialize dates as ISO8601 strings for JavaScript compatibility
- Use
.as_jsonor serializers to control what data goes to React - Component registration is required before using in views
- Initial data vs fetch: Decide if component gets data via props or fetches it
- Models don't change: React doesn't affect your ActiveRecord models
- Controllers prepare data: Controllers shape data for React consumption