How to build a single-page application (SPA) with Rails + React
A beginner-friendly guide to building a single-page application with Rails and React.
This article is written by Paulyn Ompico, freelance full stack developer, former tech lead at Growth Agence, and CTO at a concept-stage startup.
Rails and React are a dynamic duo for web development, and they make building a single-page application (SPA) surprisingly straightforward. In this tutorial, we’ll walk through creating a basic task manager app where you can add tasks and mark them as completed. It’s a simple project, but by the end, you’ll have a good foundation for creating more advanced SPAs.
Rails is known for its simplicity and convention-over-configuration approach. React, on the other hand, excels at creating interactive user interfaces. Together, they bring the best of both worlds: a robust backend and a dynamic frontend. Curious about why this pairing works so well? Dive deeper here.
In this tutorial, we’re diving into how to use Rails and React together in a decoupled setup. This means we’ll have two separate apps:
Why decoupled? It’s flexible, scalable, and great if you want your frontend and backend to evolve independently.
That said, this isn’t the only way to use Rails and React together. You can also integrate React directly into Rails, but for this tutorial, we’re going full-on decoupled.
Here’s what we’ll cover:
Prerequisite: Make sure Ruby on Rails and Node.js are installed before you start. Verify with:
rails -v node -v |
Now, let the coding begin!
Open your terminal and run:
rails new my_task_manager –api |
Once the app is created, move to its directory and open it in your favorite text editor.
cd my_task_manager code . |
* The code . command in the terminal is used to open the current directory in
Visual Studio Code.
Next, start the Rails server to see your app in action. Open a new terminal (leave the first one open) and run:
rails s |
Keep this terminal running, as it hosts your server. Now, open your browser and go to http://localhost:3000 to view your Rails app in action.
Once you see this, you’re all set to start building.
Tasks are the core of our app, so let’s start by creating a model for them. In your terminal, run:
rails generate model Task title:string completed:boolean |
This command creates:
Next, apply the migration to update your database schema:
rails db:migrate |
After running this, the tasks table will appear in your schema.rb and the Task model is now ready for use!
Now, let’s define the routes and create a controller for managing tasks.
Open config/routes.rb and add the following:
resources :tasks, only: [:index, :create, :update] |
These actions will cover the basics for our app.
In your terminal, run:
rails generate controller tasks |
This command creates a new controller file: app/controllers/tasks_controller.rb.
Replace the contents of tasks_controller.rb with this code:
class TasksController < ApplicationController def index render json: Task.all end
def create task = Task.new(task_params) if task.save render json: task else render json: { error: ‘Failed to create task’ }, status: :unprocessable_entity end end def update task = Task.find(params[:id]) if task.update(task_params) render json: task else render json: { error: ‘Failed to update task’ }, status: :unprocessable_entity end end private def task_params params.require(:task).permit(:title, :completed) end end |
Here’s what each action does:
The task_params method ensures only title and completed are accepted from the request for security.
To ensure everything is working, let’s test our API.
1. Add sample data
Let’s populate the database with some tasks using Rails seeds. Open the db/seeds.rb file and add instances of Task:
Task.destroy_all Task.create(title: “Buy groceries”, completed: false) Task.create(title: “Feed the fish”, completed: true) |
Then, run the seeding command:
rails db:seed |
2. Test in the browser
Open your browser and navigate to http://localhost:3000/tasks. You should see a JSON response containing your seeded tasks, like this:
3. Use Postman for advanced testing
Postman is very useful for testing APIs. You can either download the app or test directly online.
All you need to do is to create a new request with the correct URL, such as http://localhost:3000/tasks for testing our index.
Then you can also test various HTTP methods (e.g., GET, POST, PUT, DELETE) to interact with your API endpoints.
To test our create for example, use POST and include a JSON body, like:
{ “title”: “Water the plants”, “completed”: false } |
Confirm your API handles the requests correctly.
This way, you’ll ensure your Rails API is functional and ready to connect with your React frontend!
Now we have a functioning Rails API for our tasks! Let’s move on to creating a React app for our frontend.
Run the following command in your terminal to generate the React app:
npx create-react-app my-task-manager-frontend |
Once the app is created, move to its directory and open it in your text editor.
cd my-task-manager-frontend |
Start the React development server in a new terminal (keep the first terminal open):
npm start |
This will launch your app in the browser at http://localhost:3000. If your Rails server is already running, React may load at http://localhost:3001 instead. Leave this terminal running while you work.
Once you see this, we can now move on to building our frontend.
In the src folder, open App.js and replace its contents with the following:
import React from “react”; function App() { return ( <div> <h1>Task Manager</h1> </div> ); } export default App; |
Upon saving, the browser will automatically refresh and display the changes.
Next, let’s build a task manager UI with the following features:
Update the App.js file with the following code:
import React, { useState } from “react”; import ‘./App.css’; function App() { const [tasks, setTasks] = useState([ { id: 1, title: “Practice coding”, completed: false }, { id: 2, title: “Clean kitchen”, completed: true }, ]); const [newTask, setNewTask] = useState(“”); const addTask = () => { if (newTask.trim()) { setTasks([…tasks, { id: tasks.length + 1, title: newTask, completed: false }]); setNewTask(“”); } }; const toggleTaskCompletion = (taskId) => { setTasks( tasks.map((task) => task.id === taskId ? { …task, completed: !task.completed } : task ) ); }; return ( <div> <h1>Task Manager</h1> <ul> {tasks.map((task) => ( <li key={task.id} onClick={() => toggleTaskCompletion(task.id)} style={{ textDecoration: task.completed ? “line-through” : “none”, cursor: “pointer”, }} > {task.title} </li> ))} </ul> <input type=“text” value={newTask} onChange={(e) => setNewTask(e.target.value)} placeholder=“Add a new task” /> <button onClick={addTask}>Add Task</button> </div> ); } export default App; |
Let’s add some styling by replacing the contents of src/App.css with this:
body { font-family: Arial, sans-serif; background-color: #f4f4f4; margin: 0; padding: 0; } h1 { text-align: center; margin: 20px 0; } ul { list-style: none; padding: 0; max-width: 400px; margin: 20px auto; } li { padding: 10px; background: #fff; border: 1px solid #ddd; margin-bottom: 5px; border-radius: 5px; } input { display: block; margin: 20px auto; padding: 10px; width: 90%; max-width: 400px; border: 1px solid #ddd; border-radius: 5px; } button { display: block; margin: 10px auto; padding: 10px 20px; background: #007bff; color: #fff; border: none; border-radius: 5px; cursor: pointer; } button:hover { background: #0056b3; } |
Our browser should now display this:
Your app should now:
Note: The data is stored in memory, so refreshing the page will reset the task list to its original state.
Have fun with the layout and styling. Once you’re satisfied, we can proceed to connecting this React app to the Rails API.
Let’s replace the hardcoded tasks in your React app with real data from your Rails API. You’ll also be able to add tasks and mark them as completed, with everything synced to your backend.
We’ll use Axios to make HTTP requests. Run this in your terminal to install it:
npm install axios |
import React, { useState, useEffect } from “react”; import axios from “axios”; import “./App.css”; function App() { const [tasks, setTasks] = useState([]); const [newTask, setNewTask] = useState(“”); // Load tasks when the app starts useEffect(() => { axios.get(“http://localhost:3000/tasks”) .then((response) => { setTasks(response.data); }) .catch((error) => { console.error(“Couldn’t fetch tasks:”, error); }); }, []); const addTask = () => { if (newTask.trim()) { axios.post(“http://localhost:3000/tasks”, { title: newTask, completed: false }) .then((response) => { setTasks([…tasks, response.data]); // Add the new task setNewTask(“”); }) .catch((error) => { console.error(“Couldn’t add the task:”, error); }); } }; const toggleTaskCompletion = (taskId, completed) => { axios.put(`http://localhost:3000/tasks/${taskId}`, { completed: !completed }) .then((response) => { setTasks( tasks.map((task) => task.id === taskId ? response.data : task ) ); }) .catch((error) => { console.error(“Couldn’t update the task:”, error); }); }; return ( <div> <h1>Task Manager</h1> <ul> {tasks.map((task) => ( <li key={task.id} onClick={() => toggleTaskCompletion(task.id, task.completed)} style={{ textDecoration: task.completed ? “line-through” : “none”, cursor: “pointer”, }} > {task.title} </li> ))} </ul> <input type=“text” value={newTask} onChange={(e) => setNewTask(e.target.value)} placeholder=“Add a new task” /> <button onClick={addTask}>Add Task</button> </div> ); } export default App; |
Your Rails API might block requests from your React app unless we allow it. If that is the case, you’ll see these error messages if you inspect the page and look in your browser’s console.
To fix this, let’s go back to our Rails API app and add this in our gemfile:
gem ‘rack-cors’ |
Making sure you are in the correct directory, run:
bundle install |
Add this in config/initializers/cors.rb:
Rails.application.config.middleware.insert_before 0, Rack::Cors do allow do origins “http://localhost:3001” # Your React app’s address resource “*”, headers: :any, methods: [:get, :post, :put, :patch, :delete, :options, :head] end end |
Finally, restart your Rails server and check out your React app on the browser.
Your SPA is complete!
And that is it! You’ve built a simple single-page application using Rails and React. From setting up the backend to building dynamic components, you’ve tackled the key steps of SPA development. But this is just the beginning. There’s so much more you can do!
Keep experimenting, adding new features, and pushing your app further. Rails and React have plenty of tools to take your app to the next level. Have fun building amazing things and keep coding!
What motivates students to spend their summer holidays on learning how to code? Paulina, a