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.
Two people working together
Summary

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.

 

Why Rails + React?

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.

 

What we’ll cover

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:

  • A Rails API that handles all the backend magic, like storing and managing data.
  • A React frontend that calls the API to display everything for users to see and interact with.

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:

  1. Setting up the Rails API – We’ll get the backend up and running to handle requests and data.
  2. Creating the React app – Time to build a clean frontend.
  3. Connecting the backend and frontend – Make them talk to each other via API calls.

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!

 


 

Part 1: Setting up the Rails API

1. Create a new Rails app

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.

 

2. Generate a Task model

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:

  • A migration file in db/migrate/, which defines the structure of the tasks table.
  • A Task model file in app/models/task.rb, where the business logic of your model goes.

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!

 

3. Add routes and a controller

Now, let’s define the routes and create a controller for managing tasks.

1) Add routes

Open config/routes.rb and add the following:

resources :tasks, only: [:index, :create, :update]  
  • index is for listing all tasks.
  • create is for adding new tasks.
  • update is for modifying existing tasks.

These actions will cover the basics for our app.

 

2) Generate the Tasks controller

In your terminal, run:

rails generate controller tasks

This command creates a new controller file: app/controllers/tasks_controller.rb.

 

3) Setup the controller

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:

  • index: Fetches and returns all tasks as JSON.
  • create: Adds a new task with the provided title and completed status.
  • update: Updates an existing task based on its ID.

The task_params method ensures only title and completed are accepted from the request for security.

 

4) Testing our API

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.

 

Part 2: Creating the React app

1. Create the React app

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.

 

2. Build the layout

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:

  • Display a list of tasks.
  • Add new tasks.
  • Mark tasks as completed.

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:

  • Display a list of hardcoded tasks.
  • Allow you to add new tasks using the input box.
  • Enable toggling the completion status by clicking on them.

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.

 

Part 3: Connecting the backend to the frontend

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.

1. Install Axios

We’ll use Axios to make HTTP requests. Run this in your terminal to install it:

npm install axios

 

2. Fetch tasks from backend by updating our code in src/App.js:

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;

 

3. Fix CORS issues

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!

Our users have also consulted:
Pour développe mes compétences
Formation développeur web
Formation data scientist
Formation data analyst
Les internautes ont également consulté :
Paulina: bringing together technology and socio-economics

What motivates students to spend their summer holidays on learning how to code? Paulina, a

Suscribe to our newsletter

Receive a monthly newsletter with personalized tech tips.