Building a Full Stack Web App With Rails and React

Building web applications has become essential in today's digital world. As an aspiring web developer, React and Rails are two essential tools that can help you build robust and scalable web applications. React is an open-source JavaScript library that allows developers to build complex user interfaces with ease, while Rails is a full-stack web framework that provides the necessary tools to develop server-side applications. Together, React and Rails can be a powerful combination for building web applications.

I know we can build a full-stack app just with Rails because Rails is a full-stack framework. But this article is for the people who want to build an API with Rails and use React to build the UI.

In this article, we will explore how to build a web app using React and Rails, starting from scratch.

We will build a Todo App, to make it simple. First, we are going to build the API. And then, we will build the UI with React to consume the API created.

The article provides instructions on enabling CORS, creating models, controllers, and routes, as well as performing CRUD operations and making HTTP requests to test the endpoints using an HTTP client.

Rails

Rails is a web application development framework written in the Ruby programming language. It is designed to make assumptions about what every developer needs to make web applications easier.

Requirements

  • Ruby installed

  • Rails installed

  • NodeJs installed

Creating the REST API

We choose a directory where we want to develop our application and write the following command to create a new Rails API:

rails new todo_api --api

Enable CORS

We want our React app to consume the REST API consumes the REST API, so we have to enable CORS first.

We go to config/initializer/cors.rb. We will a file like this:

# Be sure to restart your server when you modify this file.

# Avoid CORS issues when API is called from the frontend app.
# Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin AJAX requests.

# Read more: https://github.com/cyu/rack-cors

# Rails.application.config.middleware.insert_before 0, Rack::Cors do
#   allow do
#     origins "example.com"
#
#     resource "*",
#       headers: :any,
#       methods: [:get, :post, :put, :patch, :delete, :options, :head]
#   end
# end

We need to un-comment from line 8 to line 16:

# Be sure to restart your server when you modify this file.

# Avoid CORS issues when API is called from the frontend app.
# Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin AJAX requests.

# Read more: https://github.com/cyu/rack-cors

Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
     origins "*"

     resource "*",
       headers: :any,
       methods: [:get, :post, :put, :patch, :delete, :options, :head]
   end
 end

We replace origins "example.com" with origins "*".

Then, we go to Gemfile and un-comment the line where gem "rack cors" is. And run bundle install command on the terminal.

Creating the model, database, controllers, and routes.

To create the database, models and controllers folder, we run the following command:

rails g resource todo_task

We go to db/migrate/<timestamp>create_todo_task.rb to define the attributes of the todo_task table. There will be two attributes: task as a string, and completed as a boolean.

task will show what task we have pending to do, and completed will show its status.

class CreateTodoTasks < ActiveRecord::Migration[7.0]
  def change
    create_table :todo_tasks do |t|
      t.string :task
      t.boolean :completed
      t.timestamps
    end
  end
end

We run the following command in our terminal, to migrate the table:

rails db:migrate

CRUD operations

We go to app/controllers/todo_task_controller.rb to create the functions that will allow our app to perform CRUD operations.

Index

 def index
        @todo_tasks = TodoTask.all
        render json: @todo_tasks
    end

This controller dispatches a list of all tasks in the database.

Show

 def show
        @todo_task = TodoTask.find(params[:id])
        render json: @todo_task
    end

The show controller sends the task requested by its ID.

Create

 def create
        @todo_task = TodoTask.create(
            task: params[:task],
            completed: params[:completed]
        )
        render json: @todo_task
    end

This controller inserts a new task into the database. And sends the new task added as JSON to the client.

Update

def update
        @todo_task = TodoTask.find(params[:id])
        @todo_task = TodoTask.update(
            task: params[:task],
            completed: params[:completed]
        )
        render json: @todo_task
    end

The update controller extracts the ID parameter and looks for a record in the database that matches. Then, proceeds to update the record.

Destroy

def destroy
        @todo_tasks = TodoTask.all
        @todo_task = TodoTask.find(params[:id])
        @todo_task.destroy
        render json: @todo_tasks
    end

The destroy controller deletes the record according to the ID parameter in the request.

Routes

The Rails router recognizes URLs and dispatches them to a controller's action.

Rails.application.routes.draw do
  resources :todo_tasks, only: [:index, :show, :create, :update, :destroy]

end

We set up the resources the router will serve when receiving an HTTP request from a client.

Seed Data

In db/seed.rb we created two records to display when we make HTTP requests.

task_1 = TodoTask.create(task: "Buy fruits", completed: false)
task_2 = TodoTask.create(task: "Buy cheese", completed: true)

Then we run the following command to apply the changes.

rails db:seed

HTTP Requests

Now, we make HTTP requests to make sure our API behaves as we expect.

We start the server by running the following command:

rails s

Using an HTTP client, we try all the endpoints.

GET requests

Index

Show

POST request

PUT request

DELETE request

Creating the UI

We will create a separate project folder for the UI.

Installing Vite and React

In our command line, we install Vite with a React-Typescript template. This command line will create a folder for the project.

PowerShell

#npm
npm create vite@latest todoApp -- --template react-ts

#yarn
yarn create vite@latest todoApp --template react-ts
#pnpm
pnpm create vite@latest todoApp --template react-ts

After all the packages are installed we run Vite with the command:

PowerShell

npm run dev

We go to localhost:5173 and should see the Vite and React homepage.

Project Structure

TodoApp/
 public/
 src/
    assets/
    components/
    pages/
    App.css
    App.tsx
    index.css
    main.tsx
    vite-env.ts
.gitattributes
.gitignore
index.html
package-lock.json
package.json
tsconfig.json
tsconfig.node.json
vite.config.ts

Getting a list of all the tasks.

We will put all the code to perform CRUD operations in one file, Task.tsx .

So we create a new file src/components/Task.tsx and create a function to retrieve all the tasks from the API.

import React, { useState, useEffect, ChangeEvent } from "react";

const url = "http://localhost:3000/todo_tasks";

interface Task {
  id: number;
  task: string;
  completed: boolean;
} 

const AppTask: React.FC = () => {
    const [tasks, setTasks] = useState<Task[]>([]);
    const [task, setTask] = useState("");

    useEffect(() => {
        fetchtasks();
    }, []);

    const fetchtasks =  async () => {

        const response = await fetch(`${url}`)
        setTasks( await response.json());

    };

    return (
        <div>
          <h1>Tasks</h1>
          <table>
            <tr>
              <th>Task</th>
              <th>Actions</th>
            </tr>


            {tasks.map((task) => (
              <tr key={task.id}>

                  <td>{task.task}</td>
                  <td><button onClick={() => handleDeleteTask(task.id)}>Delete</button></td>
              </tr>
            ))}

          </table>

        </div>

      );

};

The component starts by importing useState which is a hook for managing the state within a component, useEffect which is a hook for handling side effects in a component, and ChangeEvent which is an interface for handling changes to form fields.

The component then defines a constant called url which holds the URL for the API endpoint that the component will fetch data from. The interface Task is also defined, which describes the structure of a task object.

Within the component body, two state variables are defined using the useState hook: tasks and task. tasks is initialized as an empty array of Task objects, and task is initialized as an empty string.

The useEffect hook is then used to call the fetchtasks function when the component mounts. The fetchtasks function uses the fetch API to make a request to the url endpoint, and then sets the tasks state variable to the response data.

The component returns some JSX that renders a table of tasks using the tasks state variable. Each row of the table displays the task property of each Task object, along with a Delete button that calls the handleDeleteTask function (which will be defined later in this article).

App.tsx

We go to src/App.tsx to add the AppTask component to the App function.

import './App.css'
import AppTask from './components/Task'

function App() {

  return (
    <div className="App">

    <AppTask/>

    </div>
  )
}

export default App

The component returns a JSX element that represents the component's structure and content when rendered in the browser. The root <div> element has a CSS class name of "App" and contains an <AppTask> component instance.

The import statement at the top of the code imports the AppTask component from a file named Task.tsx in the ./components directory.

Now, after we saved the changes, we go to localhost:5731 and we should see this page:

Create a new task

Now, we create the function to create a new task and add it to the list.



const AppTask: React.FC = () => {
   ...

    const handleAddTask = async () => {
        const newTask = { task, completed: false};
        const options =  {

            method: 'POST',
            headers: {
                'Content-type': 'application/json; charset=UTF-8',
            },
            body: JSON.stringify(newTask),

        };
        const response = await fetch(url, options)
        const data = await response.json();
        setTasks([...tasks, data]);
        setTask("");
    };
     return (
        <div>
          <h1>Tasks</h1>
          <h2>Add Task</h2>
              <form onSubmit={(e) => e.preventDefault()}>
                <input
                  type="text"
                  value={task}
                  onChange={(e) => setTask(e.target.value)}
                />
                <button onClick={() => handleAddTask()}>Add</button>
              </form>
          <table>
            <tr>
              <th>Task</th>
              <th>Actions</th>
            </tr>


            {tasks.map((task) => (
              <tr key={task.id}>

                  <td>{task.task}</td>
                  <td><button onClick={() => handleDeleteTask(task.id)}>Delete</button></td>
              </tr>
            ))}

          </table>

        </div>

      );
};

export default AppTask;

The handleAddTask function is defined to handle adding tasks to the list. The function creates a new task object with the task that is currently set in the component's state, as well as a completed property that is initialized as false. A POST request is then made to the API endpoint using the fetch API, including the new task object as the request body.

The response from the API request is then parsed using the .json() method, and the data is added to the component's state using the setTasks method and the spread operator to merge the existing tasks array with the new task data.

The setTask method is also used to reset the task state to an empty string.

The form JSX includes an input field with a button that triggers the handleAddTask function when clicked. The form also includes an onSubmit handler to prevent the default form submission behavior

Update task

const handleToggleCompleted = async (id: number) => {
        const taskToToggle = tasks.find((task) => task.id === id);
        const updatedTask = { ...taskToToggle, completed: !taskToToggle.completed };
        const options =  {

            method: 'PUT',
            headers: {
                'Content-type': 'application/json; charset=UTF-8',
            },
            body: JSON.stringify(updatedTask),

        };
        const response = await fetch(`${url}/${id}`, options)
        const data = await response.json();
        const updatedTasks = tasks.map(task =>
            task.id === id ? data : task
          );
          setTasks(updatedTasks);
    };

This function is called handleToggleCompleted, responsible for toggling the completed state of a task when clicked on.

The function takes an id argument that corresponds to the id of the task that was clicked on. The function first finds the task with the matching id by using the .find() method on the tasks array.

The next step is to create a new task object with the updated completed property. This is done by using the spread operator to create a new object with all of the properties of the original taskToToggle object, and then modify the completed property to its opposite value using the ! operator.

A PUT request is then made to the API endpoint using the fetch API, including the updated task object as the request body.

The response from the API request is then parsed using the .json() method, and the data is used to update the component's state using the setTasks method and the .map() method to replace the existing task object with the updated task object.

return (
        <div>
          ...

            {tasks.map((task) => (
              <tr key={task.id}>
                  <span
                  style={{
                    textDecoration: task.completed ? "line-through" : "none",
                  }}
                  onClick={() => handleToggleCompleted(task.id)}
                >
                  {task.task}
                  </span>  
                  <td><button onClick={() => handleDeleteTask(task.id)}>Delete</button></td>
              </tr>
            ))}
          </table>
        </div>
      );

The task's name is rendered inside a span element, with its textDecoration style is set to "line-through" when the task is completed, and "none" when it is not.

The onClick event listener is added to the span element and when it is clicked, the handleToggleCompleted function is called, passing the task's id as an argument

Delete task

const handleDeleteTask = async (id: number) => {
        fetch(`${url}/${id}`, {method:'DELETE'}).then( () => fetchtasks())
    };

This code defines the function named handleDeleteTask that takes an id parameter of type number. The function sends an HTTP DELETE request to a URL constructed by appending the id to a url variable.

After the request is successfully sent, the fetchtasks function is called.

This function re-fetches the task list from the server and updates the component's state, causing the UI to refresh to reflect the new state of the task list.

Conclusion

In conclusion, this article focuses on creating the backend and API using Rails, with detailed instructions and code snippets to help you follow along and create your API. The article covers enabling CORS, creating models, controllers, and routes, performing CRUD operations, and making HTTP requests. The source code is available on Github, and the article also provides references to additional resources for further learning.

Thank you for taking the time to read this article.

If you have any recommendations about other packages, architectures, how to improve my code, my English, or anything; please leave a comment or contact me through Twitter, or LinkedIn.

The source code of the REST API is here.

The source code of the React UI is here.

Resources

React Documentation.

Vite Guide.

Rails Guides.

Beginner's guide to creating an API from scratch using Rails.