Rails with React

Rails and React work together when you need a rich, highly interactive frontend. Rails handles the backend — database, authentication, business logic, and API endpoints. React handles the frontend — dynamic UI components, client-side state, and complex user interactions. This combination gives you the best of both worlds: Rails productivity on the server and React flexibility in the browser.

When to Use React with Rails

Use Hotwire (no React) when:
  - Standard CRUD pages with occasional interactivity
  - Comment sections, live search, inline editing
  - You want to ship fast with minimal JavaScript

Use React with Rails when:
  - Complex UI with lots of client-side state
  - Real-time dashboards with many updating parts
  - Drag-and-drop, rich text editors, complex wizards
  - Your team already knows React well

Two Approaches

Approach 1: Rails API + React SPA
  Rails app:   --api mode, serves only JSON
  React app:   Separate project (Create React App / Vite)
  Communication: React fetches from Rails JSON API

Approach 2: React Inside Rails (Hybrid)
  Rails serves HTML pages as normal
  React components mounted in specific page sections
  Useful when most pages are standard but some need rich UIs

Approach 1: Rails API Backend

Create an API-only Rails app:

rails new myapp-api --api --database=postgresql
cd myapp-api

Build your API controllers:

app/controllers/api/v1/articles_controller.rb

module Api
  module V1
    class ArticlesController < ApplicationController
      before_action :authenticate_request

      def index
        articles = Article.published.includes(:user).order(created_at: :desc)
        render json: articles.map { |a| serialize_article(a) }
      end

      def show
        article = Article.find(params[:id])
        render json: serialize_article(article)
      end

      def create
        article = current_user.articles.build(article_params)
        if article.save
          render json: serialize_article(article), status: :created
        else
          render json: { errors: article.errors.full_messages }, status: :unprocessable_entity
        end
      end

      private

      def article_params
        params.require(:article).permit(:title, :body, :published)
      end

      def serialize_article(article)
        {
          id:         article.id,
          title:      article.title,
          body:       article.body,
          published:  article.published?,
          author:     article.user.name,
          created_at: article.created_at.iso8601
        }
      end
    end
  end
end

CORS — Allow React to Call Your API

By default, browsers block requests from one origin (React app) to another origin (Rails API). Enable CORS to allow it:

Gemfile:
  gem "rack-cors"
  bundle install
config/initializers/cors.rb

Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins "http://localhost:3001",     # React dev server
            "https://myapp.com"          # production React app

    resource "/api/*",
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options],
      credentials: true
  end
end

Token Authentication for the API

rails generate migration AddApiTokenToUsers api_token:string
rails db:migrate

class User < ApplicationRecord
  before_create :generate_api_token

  private

  def generate_api_token
    self.api_token = SecureRandom.hex(32)
  end
end

app/controllers/application_controller.rb

class ApplicationController < ActionController::API
  def authenticate_request
    token = request.headers["Authorization"]&.split(" ")&.last
    @current_user = User.find_by(api_token: token)
    render json: { error: "Unauthorized" }, status: :unauthorized unless @current_user
  end

  def current_user
    @current_user
  end
end

The React Frontend

Create a React app with Vite (faster than Create React App):

npm create vite@latest myapp-frontend -- --template react
cd myapp-frontend
npm install
npm install axios react-router-dom

Set the API base URL:

src/api.js

import axios from "axios"

const api = axios.create({
  baseURL: "http://localhost:3000/api/v1",
  headers: {
    "Content-Type": "application/json"
  }
})

// Attach token from localStorage to every request
api.interceptors.request.use((config) => {
  const token = localStorage.getItem("auth_token")
  if (token) {
    config.headers.Authorization = `Bearer ${token}`
  }
  return config
})

export default api

React Component Fetching from Rails API

src/components/ArticleList.jsx

import { useState, useEffect } from "react"
import api from "../api"

export default function ArticleList() {
  const [articles, setArticles] = useState([])
  const [loading, setLoading]   = useState(true)
  const [error, setError]       = useState(null)

  useEffect(() => {
    api.get("/articles")
       .then(res  => { setArticles(res.data); setLoading(false) })
       .catch(err => { setError("Failed to load articles"); setLoading(false) })
  }, [])

  if (loading) return <p>Loading articles...</p>
  if (error)   return <p>{error}</p>

  return (
    <div>
      <h1>Articles</h1>
      {articles.map(article => (
        <div key={article.id}>
          <h2>{article.title}</h2>
          <p>By {article.author} — {article.created_at}</p>
          <p>{article.body}</p>
        </div>
      ))}
    </div>
  )
}

Login Component

src/components/Login.jsx

import { useState } from "react"
import api from "../api"

export default function Login({ onLogin }) {
  const [email,    setEmail]    = useState("")
  const [password, setPassword] = useState("")
  const [error,    setError]    = useState(null)

  const handleSubmit = async (e) => {
    e.preventDefault()
    try {
      const res = await api.post("/sessions", { email, password })
      localStorage.setItem("auth_token", res.data.token)
      onLogin(res.data.user)
    } catch {
      setError("Invalid email or password.")
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <h1>Log In</h1>
      {error && <p>{error}</p>}
      <input type="email"    value={email}    onChange={e => setEmail(e.target.value)}    placeholder="Email" />
      <input type="password" value={password} onChange={e => setPassword(e.target.value)} placeholder="Password" />
      <button type="submit">Log In</button>
    </form>
  )
}

Sessions Controller in Rails

app/controllers/api/v1/sessions_controller.rb

module Api
  module V1
    class SessionsController < ApplicationController
      skip_before_action :authenticate_request

      def create
        user = User.find_by(email: params[:email])

        if user&.valid_password?(params[:password])
          render json: {
            token: user.api_token,
            user:  { id: user.id, name: user.name, email: user.email }
          }
        else
          render json: { error: "Invalid credentials." }, status: :unauthorized
        end
      end

      def destroy
        current_user&.regenerate_api_token
        render json: { message: "Logged out." }
      end
    end
  end
end

Approach 2: React Components Inside Rails Views

Use the react-rails gem to mount React components directly in ERB views:

gem "react-rails"
bundle install
rails generate react:install
Generate a component:
rails generate react:component ArticleEditor title:string body:string

Mount it in any view:
<%= react_component("ArticleEditor", {
  title: @article.title,
  body:  @article.body,
  saveUrl: article_path(@article)
}) %>

This renders the React component server-side for the initial HTML, then hydrates it in the browser for full interactivity.

Full Stack Architecture Diagram

Browser
  |
  +-- React App (port 3001)
  |     Components, state, routing
  |     Axios HTTP calls
  |         |
  |         | GET/POST /api/v1/articles
  |         | Authorization: Bearer token
  |         v
  +-- Rails API (port 3000)
        API controllers
        ActiveRecord models
        PostgreSQL database

Deployment Options

ServiceDeploy Rails APIDeploy React App
Heroku + Netlifygit push heroku mainnetlify deploy
Heroku + VercelHeroku CLIvercel deploy
Linux serverNginx + PumaNginx serves built React files
Render.comWeb Service (Rails)Static Site (React build)

Pairing Rails with React gives you a scalable, maintainable full-stack architecture. Rails provides a robust, secure, tested backend in a fraction of the time it would take from scratch. React delivers the interactive, responsive frontend experience modern users expect. Together they cover every layer of a professional web application.

Leave a Comment

Your email address will not be published. Required fields are marked *