1. Introduction and Motivation
Why React?
- Declarative UI: build complex interfaces by composing small, reusable components.
- Virtual DOM: efficient updates, smoother user experience.
- Rich ecosystem: hooks, context, testing tools, and libraries like Redux.
- Easy to learn once you grasp JSX and component lifecycle.
Why use React in Rails?
- Leverage Rails’ backend power (ActiveRecord, routing, authentication) with React’s frontend flexibility.
- Build single-page-app-like interactions within a Rails monolith or progressively enhance ERB views.
2. Prerequisites
- Ruby 3.4.x installed (recommend using rbenv or RVM or Mise).
- Rails 8.x (we’ll install below).
- Node.js (>= 16) and npm or Yarn.
- Code editor (VS Code, RubyMine, etc.).
Why Node.js is Required for React
React’s ecosystem relies on a JavaScript runtime and package manager:
- Build tools (ESBuild, Webpack, Babel) run as Node.js scripts to transpile JSX/ES6 and bundle assets.
- npm/Yarn fetch and manage React and its dependencies from the npm registry.
- Script execution: Rails generators and custom npm scripts (e.g.
rails javascript:install:react,npm run build) need Node.js to execute.
Without Node.js, you cannot install packages or run the build pipeline necessary to compile and serve React components.
What is Node.js?
Node.js is an open-source, cross-platform JavaScript runtime built on Chrome’s V8 engine. It enables JavaScript to be executed on the server (outside the browser) and provides:
- Server-side scripting: build web servers, APIs, and backend services entirely in JavaScript.
- Command-line tools: run scripts for tasks like building, testing, or deploying applications.
- npm ecosystem: access to hundreds of thousands of packages for virtually any functionality, from utility libraries to full frameworks.
- Event-driven, non-blocking I/O: efficient handling of concurrent operations, making it suitable for real-time applications.
Node.js is the backbone that powers React’s tooling, package management, and build processes.
3. Installing Ruby 3.4 and Rails 8
1. Install Ruby 3.4.0 (example using rbenv):
# install rbenv and ruby-build if not yet installed
brew install rbenv ruby-build
rbenv install 3.4.0
rbenv global 3.4.0
ruby -v # => ruby 3.4.0p0
Check the post for using Mise as version manager: https://railsdrop.com/2025/02/11/installing-and-setup-ruby-3-rails-8-vscode-ide-on-macos-in-2025/
2. Install Rails 8:
gem install rails -v "~> 8.0"
rails -v # => Rails 8.0.x
4. Generating a New Rails 8 App
We’ll scaffold a fresh project using ESBuild for JavaScript bundling, which integrates seamlessly with React.
rails new design_studio_react \
--database=postgresql \
-j esbuild
cd design_studio_react
--database=postgresql: sets PostgreSQL as the database adapter.-j esbuild: configures ESBuild for JS bundling (preferred for React in Rails 8).
4.1 About ESBuild
ESBuild is a next-generation JavaScript bundler and minifier written in Go. Rails 8 adopted ESBuild by default for JavaScript bundling due to its remarkable speed and modern feature set:
- Blazing-fast builds: ESBuild performs parallel compilation and leverages Go’s concurrency, often completing bundling in milliseconds even for large codebases.
- Built‑in transpilation: it supports JSX and TypeScript out of the box, so you don’t need separate tools like Babel unless you have highly custom transforms.
- Tree shaking: ESBuild analyzes import/export usage to eliminate dead code, producing smaller bundles.
- Plugin system: you can extend ESBuild with plugins for asset handling, CSS bundling, or custom file types.
- Simplicity: configuration is minimal—Rails’
-j esbuildflag generates sensible defaults, and you can tweak options inpackage.jsonor a separateesbuild.config.js.
How Rails Integrates ESBuild
When you run:
rails new design_studio_react --database=postgresql -j esbuild
Rails will:
1. Install the esbuild npm package alongside react dependencies.
2. Generate build scripts in package.json, e.g.:
"scripts": {
"build": "esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds",
"build:watch": "esbuild app/javascript/*.* --bundle --sourcemap --watch --outdir=app/assets/builds"
}
3. Add a default app/assets/builds output directory and ensure Rails’ asset pipeline picks up the compiled files.
Customizing ESBuild
If you need to tweak ESBuild settings:
Add an esbuild.config.js at your project root:
const esbuild = require('esbuild')
esbuild.build({
entryPoints: ['app/javascript/application.js'],
bundle: true,
sourcemap: true,
outdir: 'app/assets/builds',
loader: { '.js': 'jsx', '.png': 'file' },
define: { 'process.env.NODE_ENV': '"production"' },
}).catch(() => process.exit(1))
Update package.json scripts to use this config:
"scripts": {
"build": "node esbuild.config.js",
"build:watch": "node esbuild.config.js --watch"
}
Why ESBuild Matters for React in Rails
- Developer experience: near-instant rebuilds let you see JSX changes live without delay.
- Production readiness: built‑in minification and tree shaking keep your asset sizes small.
- Future-proof: the plugin ecosystem grows, and Rails can adopt newer bundlers (like SWC or Vite) with a similar pattern.
With ESBuild, your React components compile quickly, your development loop tightens, and your production assets stay optimized—making it the perfect companion for a modern Rails 8 + React stack.
5. What is Virtual DOM
The Virtual DOM is one of React’s most important concepts. Let me explain it clearly with examples.
🎯 What is the Virtual DOM?
The Virtual DOM is a JavaScript representation (copy) of the actual DOM that React keeps in memory. It’s a lightweight JavaScript object that describes what the UI should look like.
📚 Real DOM vs Virtual DOM
Real DOM (What the browser uses):
<!-- This is the actual DOM in the browser -->
<div id="todo-app">
<h1>My Todo List</h1>
<ul>
<li>React List</li>
<li>Build a todo app</li>
</ul>
</div>
Virtual DOM (React’s JavaScript representation):
// This is React's Virtual DOM representation
{
type: 'div',
props: {
id: 'todo-app',
children: [
{
type: 'h1',
props: {
children: 'My Todo List'
}
},
{
type: 'ul',
props: {
children: [
{
type: 'li',
props: {
children: 'React List'
}
},
{
type: 'li',
props: {
children: 'Build a todo app'
}
}
]
}
}
]
}
}
🔄 How Virtual DOM Works – The Process
Step 1: Initial Render
// Your JSX
const App = () => {
return (
<div>
<h1>My Todo List</h1>
<ul>
<li>React List</li>
</ul>
</div>
);
};
// React creates Virtual DOM
const virtualDOM = {
type: 'div',
props: {
children: [
{ type: 'h1', props: { children: 'My Todo List' } },
{
type: 'ul',
props: {
children: [
{ type: 'li', props: { children: 'React List' } }
]
}
}
]
}
};
Step 2: State Changes
// When you add a new todo
const App = () => {
const [todos, setTodos] = useState(['React List']);
const addTodo = () => {
setTodos(['React List', 'Build Todo App']); // State change!
};
return (
<div>
<h1>My Todo List</h1>
<ul>
{todos.map(todo => <li key={todo}>{todo}</li>)}
</ul>
<button onClick={addTodo}>Add Todo</button>
</div>
);
};
Step 3: New Virtual DOM is Created
// React creates NEW Virtual DOM
const newVirtualDOM = {
type: 'div',
props: {
children: [
{ type: 'h1', props: { children: 'My Todo List' } },
{
type: 'ul',
props: {
children: [
{ type: 'li', props: { children: 'React List' } },
{ type: 'li', props: { children: 'Build Todo App' } } // NEW!
]
}
},
{ type: 'button', props: { children: 'Add Todo' } }
]
}
};
Step 4: Diffing Algorithm
// React compares old vs new Virtual DOM
const differences = [
{
type: 'ADD',
location: 'ul.children',
element: { type: 'li', props: { children: 'Build Todo App' } }
}
];
Step 5: Reconciliation (Updating Real DOM)
// React updates ONLY what changed in the real DOM
const ul = document.querySelector('ul');
const newLi = document.createElement('li');
newLi.textContent = 'Build Todo App';
ul.appendChild(newLi); // Only this line runs!
🚀 Why Virtual DOM is Fast
Without Virtual DOM (Traditional approach):
// Traditional DOM manipulation
function updateTodoList(todos) {
const ul = document.querySelector('ul');
ul.innerHTML = ''; // Clear everything!
todos.forEach(todo => {
const li = document.createElement('li');
li.textContent = todo;
ul.appendChild(li); // Recreate everything!
});
}
With Virtual DOM (React approach):
// React's approach
function updateTodoList(oldTodos, newTodos) {
const differences = findDifferences(oldTodos, newTodos);
differences.forEach(diff => {
if (diff.type === 'ADD') {
// Only add the new item
const li = document.createElement('li');
li.textContent = diff.todo;
ul.appendChild(li);
}
});
}
🎭 Real Example with Our Todo App
Let’s trace through what happens when you add a todo:
Before Adding Todo:
// Current state
const [todos, setTodos] = useState([
{ id: 1, text: 'React List', completed: false },
{ id: 2, text: 'Build Todo App', completed: false }
]);
// Virtual DOM representation
{
type: 'ul',
props: {
children: [
{ type: 'li', key: 1, props: { children: 'React List ⏳' } },
{ type: 'li', key: 2, props: { children: 'Build Todo App ⏳' } }
]
}
}
After Adding Todo:
// New state
const [todos, setTodos] = useState([
{ id: 1, text: 'React List', completed: false },
{ id: 2, text: 'Build Todo App', completed: false },
{ id: 3, text: 'Master React Hooks', completed: false } // NEW!
]);
// New Virtual DOM
{
type: 'ul',
props: {
children: [
{ type: 'li', key: 1, props: { children: 'React List ⏳' } },
{ type: 'li', key: 2, props: { children: 'Build Todo App ⏳' } },
{ type: 'li', key: 3, props: { children: 'Master React Hooks ⏳' } } // NEW!
]
}
}
React’s Diffing Process:
// React compares and finds:
const changes = [
{
type: 'INSERT',
location: 'ul',
element: { type: 'li', key: 3, props: { children: 'Master React Hooks ⏳' } }
}
];
// React updates ONLY what changed:
const ul = document.querySelector('ul');
const newLi = document.createElement('li');
newLi.textContent = 'Master React Hooks ⏳';
ul.appendChild(newLi); // Only this operation!
🎯 Key Benefits of Virtual DOM
1. Performance:
// Without Virtual DOM: Updates entire list
document.querySelector('ul').innerHTML = generateEntireList(todos);
// With Virtual DOM: Updates only what changed
document.querySelector('ul').appendChild(newTodoElement);
2. Predictability:
// You write declarative code
const TodoList = ({ todos }) => (
<ul>
{todos.map(todo => <li key={todo.id}>{todo.text}</li>)}
</ul>
);
// React handles the imperative updates
// You don't need to manually add/remove DOM elements
3. Batching:
// Multiple state updates in one event
const handleButtonClick = () => {
setTodos([...todos, newTodo]); // Change 1
setInputValue(''); // Change 2
setCount(count + 1); // Change 3
};
// React batches these into one DOM update!
🔧 Virtual DOM in Action – Debug Example
You can actually see the Virtual DOM in action:
import React, { useState } from 'react';
const App = () => {
const [todos, setTodos] = useState(['React List']);
const addTodo = () => {
console.log('Before update:', todos);
setTodos([...todos, 'New Todo']);
console.log('After update:', todos); // Still old value!
};
console.log('Rendering with todos:', todos);
return (
<div>
<ul>
{todos.map((todo, index) => (
<li key={index}>{todo}</li>
))}
</ul>
<button onClick={addTodo}>Add Todo</button>
</div>
);
};
🎭 Common Misconceptions
❌ “Virtual DOM is always faster”
// For simple apps, Virtual DOM has overhead
// Direct DOM manipulation can be faster for simple operations
document.getElementById('counter').textContent = count;
❌ “Virtual DOM prevents all DOM operations”
// React still manipulates the real DOM
// Virtual DOM just makes it smarter about WHEN and HOW
✅ “Virtual DOM optimizes complex updates”
// When you have many components and complex state changes
// Virtual DOM's diffing algorithm is much more efficient
🧠 Does React show Virtual DOM to the user?
No. The user only ever sees the real DOM.
The Virtual DOM (VDOM) is never shown directly. It’s just an internal tool used by React to optimize how and when the real DOM gets updated.
🧩 What is Virtual DOM exactly?
- A JavaScript-based, lightweight copy of the real DOM.
- Stored in memory.
- React uses it to figure out what changed after state/props updates.
👀 What the user sees:
- The real, visible HTML rendered to the browser — built from React components.
- This is called the Real DOM.
🔁 So why use Virtual DOM at all?
✅ Because manipulating the real DOM is slow.
React uses VDOM to:
- Build a new virtual DOM after every change.
- Compare (diff) it with the previous one.
- Figure out the minimum real DOM updates required.
- Apply only those changes to the real DOM.
This process is called reconciliation.
🖼️ Visual Analogy
Imagine the Virtual DOM as a sketchpad.
React draws the new state on it, compares it with the old sketch, and only updates what actually changed in the real-world display (real DOM).
✅ TL;DR
| Question | Answer |
|---|---|
| Does React show the virtual DOM to user? | ❌ No. Only the real DOM is ever visible to the user. |
| What is virtual DOM used for? | 🧠 It’s used internally to calculate DOM changes efficiently. |
| Is real DOM updated directly? | ✅ Yes, but only the minimal parts React determines from the VDOM diff. |
🧪 Example Scenario
👤 The user is viewing a React app with a list of items and a button:
<ul>
<li>Item 1</li>
<li>Item 2</li>
...
<li>Item 10</li>
</ul>
<button>Read More</button>
When the user clicks “Read More”, the app adds 10 more items to the list.
🧠 Step-by-Step: What Happens Behind the Scenes
✅ 1. User Clicks “Read More” Button
<button onClick={loadMore}>Read More</button>
This triggers a React state update, e.g.:
function loadMore() {
setItems([...items, ...next10Items]); // updates state
}
🔁 State change → React starts re-rendering
📦 2. React Creates a New Virtual DOM
- React re-runs the component’s render function.
- This generates a new Virtual DOM tree (just a JavaScript object structure).
Example of the new VDOM:
{
type: "ul",
children: [
{ type: "li", content: "Item 1" },
...
{ type: "li", content: "Item 20" } // 10 new items
]
}
🧮 3. React Diffs New Virtual DOM with Old One
- Compares previous VDOM (10
<li>items) vs new VDOM (20<li>items). - Finds that 10 new
<li>nodes were added.
This is called the reconciliation process.
⚙️ 4. React Updates the Real DOM
- React tells the browser:
“Please insert 10 new<li>elements inside the<ul>.”
✅ Only these 10 DOM operations happen.
❌ React does not recreate the entire <ul> or all 20 items.
🖼️ What the User Sees
On the screen (the real DOM):
<ul>
<li>Item 1</li>
...
<li>Item 20</li>
</ul>
The user never sees the Virtual DOM — they only see the real DOM updates that React decides are necessary.
🧠 Summary: Virtual DOM vs Real DOM
| Step | Virtual DOM | Real DOM |
|---|---|---|
| Before click | 10 <li> nodes in memory | 10 items visible on screen |
| On click | New VDOM generated with 20 <li> nodes | React calculates changes |
| Diff | Compares new vs old VDOM | Determines: “Add 10 items” |
| Commit | No UI shown from VDOM | Only those 10 new items added to browser DOM |
✅ Key Point
🧠 The Virtual DOM is a tool for React, not something the user sees.
👁️ The user only sees the final, optimized changes in the real DOM.
🎯 Summary
Virtual DOM is React’s:
- JavaScript representation of the real DOM
- Diffing algorithm that compares old vs new Virtual DOM
- Reconciliation process that updates only what changed
- Performance optimization for complex applications
- Abstraction layer that lets you write declarative code
Think of it as React’s smart assistant that:
- Remembers what your UI looked like before
- Compares it with what it should look like now
- Makes only the necessary changes to the real DOM
This is why you can write simple, declarative code like {todos.map(todo => <li>{todo}</li>)} and React handles all the complex DOM updates efficiently!
🔄 After the Virtual DOM Diff, How React Updates the Real DOM
🧠 Step-by-Step:
- React creates a diff between the new and previous virtual DOM trees.
- React then creates a list of “instructions” called the update queue.
- Examples:
- “Insert
<li>Item 11</li>at position 10″ - “Remove
<div>at index 3″ - “Change text of button to ‘Read Less'”
- “Insert
- Examples:
- These changes are passed to React’s reconciliation engine.
- React uses the browser’s DOM APIs (
document.createElement,appendChild,removeChild, etc.) to apply only the minimal changes.
✅ So instead of doing:
document.body.innerHTML = newHTML; // inefficient, replaces all
React does:
const newEl = document.createElement("li");
newEl.textContent = "Item 11";
ul.appendChild(newEl); // just this
❓ Why Didn’t Browsers Do This Earlier?
Excellent historical question. The short answer is: Browsers give us the tools, but React gave us the strategy.
⚠️ Why browsers didn’t do it automatically:
| Reason | Explanation |
|---|---|
| 🧱 Low-level APIs | The browser exposes DOM APIs (appendChild, setAttribute), but they’re imperative — devs must write the logic. |
| 🤯 Complexity | Managing DOM efficiently across many updates (nested, reordered, conditional elements) is hard and bug-prone manually. |
| 🔁 Manual state syncing | Before React, developers had to manually keep UI in sync with state. That logic got complex and messy fast. |
| 📦 No built-in abstraction | Browsers don’t offer a built-in “virtual diff engine” or abstraction like React’s VDOM. |
🤖 What React Added That Browsers Don’t
| Feature | Browser DOM | React (with VDOM) |
|---|---|---|
| Efficient diffing | ❌ No | ✅ Yes (reconciliation) |
| Declarative UI | ❌ No | ✅ Yes (return <UI />) |
| Component abstraction | ❌ No | ✅ Yes (function/class components) |
| State-driven rendering | ❌ Manual | ✅ Built-in |
| Minimal updates | ❌ Up to you | ✅ Automatic via VDOM |
✅ TL;DR
- React calculates exactly what changed via the virtual DOM diffing.
- It then uses native DOM APIs to update only what’s necessary in the real DOM.
- Browsers give you low-level control, but not an optimized strategy for updating UI based on state — React filled that gap beautifully.
Now Let’s break down how a React app starts after you run:
npx create-react-app my-app
cd my-app
npm start
What actually happens behind the scenes? Let’s unpack it step-by-step 👇
⚙️ Step 1: npx create-react-app — What It Does
This command:
- Downloads and runs the latest version of the
create-react-apptool (CRA). - Sets up a project with:
- A preconfigured Webpack + Babel build system
- Development server
- Scripts and dependencies
- Installs React, ReactDOM, and a bunch of tools inside
node_modules.
Key folders/files created:
my-app/
├── node_modules/
├── public/
├── src/
│ └── index.js 👈 main entry point
├── package.json
Step 2: npm start — How the App Runs
When you run:
npm start
It’s actually running this line from package.json:
"scripts": {
"start": "react-scripts start"
}
So it calls:
react-scripts start
🧠 What is react-scripts?
react-scripts is a package from Facebook that:
- Runs a development server using Webpack Dev Server
- Compiles JS/JSX using Babel
- Watches your files for changes (HMR)
- Starts a browser window at
http://localhost:3000
It configures:
- Webpack
- Babel
- ESLint
- PostCSS
- Source maps
… all behind the scenes, so you don’t have to set up any configs manually.
📦 Libraries Involved
| Tool / Library | Purpose |
|---|---|
| React | Core UI library (react) |
| ReactDOM | Renders React into actual DOM (react-dom) |
| Webpack | Bundles your JS, CSS, images, etc. |
| Babel | Converts modern JS/JSX to browser-friendly JS |
| Webpack Dev Server | Starts dev server with live reloading |
| react-scripts | Runs all the above with pre-made configs |
🏗️ Step 3: Entry Point — src/index.js
The app starts here:
// src/index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
ReactDOM.createRoot(...)finds the<div id="root">inpublic/index.html.- Then renders the
<App />component into it. - The DOM inside the browser updates — and the user sees the UI.
✅ TL;DR
| Step | What Happens |
|---|---|
npx create-react-app | Sets up a full React project with build tools |
npm start | Calls react-scripts start, which runs Webpack dev server |
react-scripts | Handles build, hot reload, and environment setup |
index.js | Loads React and renders your <App /> to the browser DOM |
| Browser Output | You see your live React app at localhost:3000 |
6. Installing and Configuring React
Rails 8 provides a generator to bootstrap React + ESBuild.
- Run the React installer:
rails javascript:install:react
This will:- Install
reactandreact-domvia npm. - Create an example
app/javascript/components/HelloReact.jsxcomponent. - Configure ESBuild to transpile JSX.
- Install
- Verify your application layout:
Inapp/views/layouts/application.html.erb, ensure you have:<%= javascript_include_tag "application", type: "module", defer: true %> - Mount the React component:
Replace (or add) a div placeholder in an ERB view, e.g.app/views/home/index.html.erb:<div id="hello-react" data-props="{}"></div> - Initialize mount point
Inapp/javascript/application.js:
import "./components"
In app/javascript/components/index.js:
import React from "react"
import { createRoot } from "react-dom/client"
import HelloReact from "./HelloReact"
document.addEventListener("DOMContentLoaded", () => {
const container = document.getElementById("hello-react")
if (container) {
const root = createRoot(container)
const props = JSON.parse(container.dataset.props || "{}")
root.render(<HelloReact {...props} />)
}
})
Your React component will now render within the Rails view!
See you in Part 2 … 🚀