Rails: React Front-end with Rails API CRUD application

Umair Khalid
13 min readFeb 1, 2023

--

In this article, I will be giving a brief tutorial on how to get connect a React front-end application with a Rails API CRUD application.

Lets start with the Rails part of our little project. I will setup a very simple CRUD application with a students table which has properties of name, email and roll number. I will create the new application using the command listed below and make the model and controller. We don’t need views as our front-end is on React.

rails new rails-react-backend - api
rails g model student name:string email:string rollnumber:integer
rails g controller StudentsController
rails db:migrate

Now I will move my newly generated controller file to controllers/api/v1 namespace so we can separate our API controllers. Now let’s make the common CRUD actions for our controller.

#students_controller.rb
class Api::V1::StudentsController < ApplicationController
before_action :set_student, only: [:edit, :update, :destroy]
def index
@students = Student.all
render json: @students
end

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

def create
@student = Student.new(student_params)
if @student.save
render json: { message: 'Record Created successfully' }, status: :created
else
render json: @student.errors, status: :unprocessable_entity
end
end

def edit
render json: @student
end

def update
if @student.update(student_params)
render json: { message: 'Record Updated successfully' }, status: :ok
else
render json: @student.errors, status: :unprocessable_entity
end
end

def destroy
if @student.destroy
render json: { message: 'Record Deleted successfully' }, status: :ok
else
render json: @student.errors, status: :unprocessable_entity
end
end

private

def student_params
params.fetch(:student, {}).permit(:name, :email, :rollnumber)
end

def set_student
@student = Student.find(params[:id])
end
end
#student.rb
class Student < ApplicationRecord
default_scope { order(rollnumber: :asc) }
validates :name, :email, :rollnumber, presence: true
validates :email, format: { with: URI::MailTo::EMAIL_REGEXP }
validates :rollnumber, numericality: { only_integer: true }
end

This is standard Rails stuff. Notice that I am rendering JSON because that is the format in which we want our React front-end to receive the data. I have also applied so some validations so we can test our application for error messages as well.

Lets manage our routes as well, keeping in mind where our controller’s location is.

#routes.rb
Rails.application.routes.draw do
namespace :api do
namespace :v1, defaults: { format: :json } do
resources :students
end
end
end

Last thing, right now I am using localhost:3000 for the React app and localhost:5000 for my Rails app. Run your Rails app and React app with the following commands respectively:

rails s -p 5000
npm start

Now there is one problem that will happen, which I am going to preemptively solve. If our twoapplications communicate, we will get a CORS error.

CORS (Cross-Origin Resource Sharing) is a security feature implemented by web browsers that prevents a web page from making requests to a different domain than the one that served the web page. When a browser receives a CORS error, it means that the server at the requested domain is not allowing the browser to access its resources.

One way to solve this is by using the “rack-cors” gem. Write that in your gem file.

gem 'rack-cors'

Then create a new file cors.rb under config/initializers/ and paste the following content. This will allow our React app to send requests to our Rails API and our Rails API to send back a response on localhost.

#cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins 'http://localhost:3000'
resource '*', headers: :any, methods: [:get, :post, :put, :patch, :delete, :options, :head], credentials: true
end
end

And that will be all from the Rails side. Now lets move on to the React part.

Let me discuss how I will be approaching the React application. There will be an index page, where all the student data is displayed in table form, as well as edit, delete and show buttons. Each of them which will navigate to different pages. There will be a create button at the bottom as well. I will be using React Router for navigation between the pages which is very straight-forward to use, as well as some Bootstrap so our styling is not so bland. I will also be using React States and Hooks. No let’s crack on.

First we will create a new React application, and install React Router and Bootstrap. Don’t forget to import them as well where needed.

npx create_react_app rails-react-frontend
npm install bootstrap
npm install react-router-dom

Now go ahead and remove any unnecessary JavaScript files . I will show you the structure of my project, feel free to follow it.

Go ahead and create those 5 JSX files — StudentCreate.jsx, StudentEdit.jsx, StudentIndex.jsx, StudentShow.jsx, StudentForm.jsx.

Now let’s setup our index.js and App.jsx component.

#index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
import 'bootstrap/dist/css/bootstrap.min.css';
import 'bootstrap/dist/js/bootstrap.bundle.min';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<App />
</React.StrictMode>
);

Here I have imported Bootstrap and rendered the App component.

#App.jsx
import { BrowserRouter, Routes, Route } from "react-router-dom";
import StudentIndex from "./components/StudentIndex";
import StudentShow from "./components/StudentShow";
import StudentCreate from "./components/StudentCreate";
import StudentEdit from "./components/StudentEdit";

export default function App() {
return (
<BrowserRouter>
<div>
<Routes>
<Route path='/' exact element={<StudentIndex />} />
<Route path='/students/index' exact element={<StudentIndex />} />
<Route path='/students/:id/show' exact element={<StudentShow />} />
<Route path='/students/create' exact element={<StudentCreate />} />
<Route path='/students/:id/edit' exact element={<StudentEdit />} />
</Routes>
</div>
</BrowserRouter>
);
}

Here I have imported React Router components that I need, as well as my four student components. I have gone ahead and setup up the routes as well. They do not need to match your Rails routes in any way. With each route I am providing its specific component as a prop. The exact keyword matches the route exactly with what has been given. Note that the :id part is dynamic and will be replaced by a number value.

Let’s setup our index page retrieve all students and display them.

#StudentIndex.jsx
import React, { useState, useEffect } from "react";

export default function StudentList() {
const [students, setStudents] = useState([]);

useEffect(() => {
fetch("http://localhost:5000/api/v1/students")
.then((response) => response.json())
.then((data) => setStudents(data))
.catch((error) => console.log(error));
}, []);

return (
<section className='m-5'>
<h1 className='h1 text-center'>Student Info</h1>
<h3>Student Index</h3>
<table className='table table-striped'>
<thead>
<tr>
<th scope='col'>ID</th>
<th scope='col'>Name</th>
<th scope='col'>Email</th>
<th scope='col'>Roll Number</th>
</tr>
</thead>
<tbody>
{students.map((student) => (
<tr key={student.id}>
<th scope='row'>{student.id}</th>
<td className="fw-bold">{student.name}</td>
<td>{student.email}</td>
<td>{student.rollnumber}</td>
</tr>
))}
</tbody>
</table>
</section>
);
}

Let’s start from the top. I imported the useState() and useEffect() hooks, which are the 2 most common hooks used in React. I then make a state students, as well as a function to set it. This is the conventional way of declaring a state. This state will be used to store the student collection I get from the API. Initially it will be empty as I have passed nothing to it.

In the next part, I am using the useEffect() hook to make a GET request. It is not mentioned anywhere that it is a GET request because by default if you do not define the method/headers the request is a GET type.

This hook will run on every render of the page. I make the request using fetch() and provide the URL I want to hit. You can find the specific routes by viewing your Rails Routes.

The first line is a JavaScript Promise, which on success returns a response object sent by the API. I then tack a then() function which is another promise, to convert the response to JSON format, which returns a data object, that is the raw data from our controller’s index action. The last line is to catch any errors and log them in the console.

In the return statement I have made a Bootstrap table, and by using the student state in a map() function , I simply output the properties here.

Now let’s implement the delete action, the simplest of the 4. I will add a delete button, and a function to handle that action.

#StudentIndex.jsx
import React, { useState, useEffect } from "react";

export default function StudentList() {
const [students, setStudents] = useState([]);

useEffect(() => {
fetch("http://localhost:5000/api/v1/students")
.then((response) => response.json())
.then((data) => setStudents(data))
.catch((error) => console.log(error));
}, []);

function handleDelete(id) {
fetch(`http://localhost:5000/api/v1/students/${id}`, {
method: "DELETE",
headers: { "Content-Type": "application/json" },
})
.then((response) => response.json())
.then((data) => { setStudents(students.filter((student) => student.id !== id));})
.catch((error) => console.log(error));
}

return (
<section className='m-5'>
<h1 className='h1 text-center'>Student Info</h1>
<h3>Student Index</h3>
<table className='table table-striped'>
<thead>
<tr>
<th scope='col'>ID</th>
<th scope='col'>Name</th>
<th scope='col'>Email</th>
<th scope='col'>Roll Number</th>
</tr>
</thead>
<tbody>
{students.map((student) => (
<tr key={student.id}>
<th scope='row'>{student.id}</th>
<td className="fw-bold">{student.name}</td>
<td>{student.email}</td>
<td>{student.rollnumber}</td>
<td>
<button className='btn btn-danger mx-2' onClick={() => handleDelete(student.id)}>Delete</button>
</td>
</tr>
))}
</tbody>
</table>
</section>
);
}

The button that I added calls the handleDelete() function and also passes the id which we need to find the record in the database. In the function itself, this time I have specified the method and headers because I want a DELETE request. The only other thing that I am doing different here from the GET request is filter the students array and show all objects expect the one with the id I just passed. This will make the record disappear live instead of us having to reload the page to see the effect.

Now on to the show action. For this, again we will create a button and a function to handle it. But this time we navigate to another page. Let’s see how it is done.

#StudentIndex.jsx
import React, { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";

export default function StudentList() {
const [students, setStudents] = useState([]);
const navigate = useNavigate();

useEffect(() => {
fetch("http://localhost:5000/api/v1/students")
.then((response) => response.json())
.then((data) => setStudents(data))
.catch((error) => console.log(error));
}, []);

function handleDelete(id) {
fetch(`http://localhost:5000/api/v1/students/${id}`, {
method: "DELETE",
headers: { "Content-Type": "application/json" },
})
.then((response) => response.json())
.then((data) => { setStudents(students.filter((student) => student.id !== id));})
.catch((error) => console.log(error));
}

const handleShow = (id) => {
navigate(`/students/${id}/show`);
};

return (
<section className='m-5'>
<h1 className='h1 text-center'>Student Info</h1>
<h3>Student Index</h3>
<table className='table table-striped'>
<thead>
<tr>
<th scope='col'>ID</th>
<th scope='col'>Name</th>
<th scope='col'>Email</th>
<th scope='col'>Roll Number</th>
</tr>
</thead>
<tbody>
{students.map((student) => (
<tr key={student.id}>
<th scope='row'>{student.id}</th>
<td className="fw-bold">{student.name}</td>
<td>{student.email}</td>
<td>{student.rollnumber}</td>
<td>
<button className='btn btn-primary mx-2' onClick={() => handleShow(student.id)}>Show</button>
<button className='btn btn-danger mx-2' onClick={() => handleDelete(student.id)}>Delete</button>
</td>
</tr>
))}
</tbody>
</table>
</section>
);
}

There are multiple ways to do this, I chose the one simplest to understand. I use the the useNavigate() hook to navigate to the show page. Now remember, this route must match the route you defined on the App.jsx file. This will be matched there, and then the component we specified will be rendered there, and we also receive the id as a parameter from the URL, which I will show you how to retrieve.

Let’s fill our StudentShow.jsx file. Here, I will again make a GET request, but this time with an id, to get a specific record, and then I just display it. Again, if you have any confusion about how to write your routes, check your rails routes.

#StudentShow.jsx
import React, { useState, useEffect } from 'react';
import { Link, useParams } from 'react-router-dom';

export default function StudentShow() {
const [student, setStudent] = useState({});
const {id} = useParams();

useEffect(() => {
fetch(`http://localhost:5000/api/v1/students/${id}`)
.then(response => response.json())
.then(data => setStudent(data))
.catch(error => console.log(error));
}, [id]);

return (
<div className="m-5">
<h3 className="m-5">Student Show</h3>
<div className="m-5" v>
<p><strong>ID:</strong> {student.id}</p>
<p><strong>Name:</strong> {student.name}</p>
<p><strong>Email:</strong> {student.email}</p>
<p><strong>Roll Number:</strong> {student.rollnumber}</p>
<Link to="/students/index">Back to Home</Link>
</div>
</div>
);
}

I use a state to store the student object, when I make a request to the API and get back the response. Any error messages will go to our console. The biggest new thing here is the useParams() hook. This is a hook in React that allows you to access the dynamic values in the URL path parameters of a React Router component. It is used to extract values from the URL path and make them available to your component. So we get our id value here and use it in our request. I have also use a Link component from React Router to link back to our index page.

Moving on, let’s start our create and edit components. I have mentioned these 2 together because they both require a form. I want to make a single form which can be used by both components. The difference will be that when the create component renders the form, the form fields will be empty and I will want the form to use the function I provide for the create action on submit. In the edit action, the form fields will already be populated by the previous data which can be further edited, and I will want the form to use the function I provide for the edit action on submit.

Let’s now write our create component as discussed.

#StudentCreate.jsx
import { useNavigate } from "react-router-dom";
import StudentForm from "./StudentForm";

export default function StudentCreate() {
const navigate = useNavigate();

function handleSubmit(formData) {
fetch("http://localhost:5000/api/v1/students", {
method: "POST",
headers: { "Content-Type": "application/json", },
body: JSON.stringify(formData),
})
.then((response) => {
if (response.ok) {
navigate(`/students/index`);
return response.json();
}
})
.then((data) => console.log(data.message))
.catch((error) => console.error(error));
};

return (
<StudentForm student={{}} handleSubmit={handleSubmit} />
);
}

Here, I make a POST request to the API, and pass the form data (which I will show you how it receives in a bit) in JSON format. Then, if the response returns with a status of 200 or ok (which we defined in our Rails controller, go back and check), I navigate back to the index page, otherwise I will stay there and any error message will be logged in the console. Then I call the form component (I will make it after the edit component) with props for student and handleSubmit() function. Yes, we can pass functions as parameters in React. The student prop is empty because I want the form fields to be initially empty in this case.

In the same way (almost the same way), we will make our edit component.

#StudentEdit.jsx
import React, { useState, useEffect } from "react";
import { useParams, useNavigate } from "react-router-dom";
import StudentForm from "./StudentForm";

export default function StudentEdit() {
const [student, setStudent] = useState({});
const navigate = useNavigate();
const {id} = useParams();

useEffect(() => {
fetch(`http://localhost:5000/api/v1/students/${id}`)
.then((response) => response.json())
.then((data) => setStudent(data))
.catch((error) => console.error(error));
}, [id]);

const handleSubmit = (formData) => {
fetch(`http://localhost:5000/api/v1/students/${id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json", },
body: JSON.stringify(formData),
})
.then((response) => {
if (response.ok) {
navigate(`/students/index`);
return response.json()
}
})
.then((data) => console.log(data.message))
.catch((error) => console.error(error));
};

return (
<StudentForm student={student} handleSubmit={handleSubmit} />
);
};

Here, I again use the useParams() hook to get the id from the URL. Now I use the useEffect() hook to get our student object, and store it in our state, just like we did in the show action. Initially the state is empty. Notice the problem and how to deal with it ? I’ll tell you in a bit. Next we define our submit function where we make a PATCH request to the API and pass our form data as JSON. Then we either redirect to our index page our stay there in case of an error, same as the create component.

Now onto the problem I mentioned before. I render the form component again with the props for student and handleSubmit() function. The thing is, initially the student state is empty. So when I render the form component, it will receive a null/undefined value for the student prop. So the form fields will be and remain empty, regardless of me fetching the data from the API in useEffect(). So I need to only render the form component if the data has been fetched and the student is populated. So we return this piece of code:

return (
<>
{student.id ? (<StudentForm student={student} handleSubmit={handleSubmit} />) :(<div>Loading...</div>)}
</>
);

Here, only if the student object exists, we will render our form component. Problem solved.

Don’t forget, we also need to add buttons and their specific functions to our index page as well. This will be the same way as we did the previous 2 buttons.

#StudentIndex.jsx
import React, { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";

export default function StudentList() {
const [students, setStudents] = useState([]);
const navigate = useNavigate();

useEffect(() => {
fetch("http://localhost:5000/api/v1/students")
.then((response) => response.json())
.then((data) => setStudents(data))
.catch((error) => console.log(error));
}, []);

function handleDelete(id) {
fetch(`http://localhost:5000/api/v1/students/${id}`, {
method: "DELETE",
headers: { "Content-Type": "application/json" },
})
.then((response) => response.json())
.then((data) => { setStudents(students.filter((student) => student.id !== id));})
.catch((error) => console.log(error));
}

const handleShow = (id) => {
navigate(`/students/${id}/show`);
};

const handleCreate = () => {
navigate(`/students/create`);
};

const handleEdit = (id) => {
navigate(`/students/${id}/edit`);
};

return (
<section className='m-5'>
<h1 className='h1 text-center m-5'>Student Info</h1>
<table className='table table-striped m-5'>
<thead>
<tr>
<th scope='col'>ID</th>
<th scope='col'>Name</th>
<th scope='col'>Email</th>
<th scope='col'>Roll Number</th>
</tr>
</thead>
<tbody>
{students.map((student) => (
<tr key={student.id}>
<th scope='row'>{student.id}</th>
<td className="fw-bold">{student.name}</td>
<td>{student.email}</td>
<td>{student.rollnumber}</td>
<td>
<button className='btn btn-primary mx-2' onClick={() => handleShow(student.id)}>Show</button>
<button className='btn btn-success mx-2' onClick={() => handleEdit(student.id)}>Edit</button>
<button className='btn btn-danger mx-2' onClick={() => handleDelete(student.id)}>Delete</button>
</td>
</tr>
))}
</tbody>
</table>
<button className='btn btn-dark mx-5' onClick={() => handleCreate()}>Create New Student</button>
</section>
);
}

Now lastly, let’s finally make the form component that I have mentioned so much.

#StudentForm.jsx
import { useState } from 'react';

export default function StudentForm({ student, handleSubmit }) {
const [formData, setFormData] = useState({
name: student.name || '',
email: student.email || '',
rollnumber: student.rollnumber || '',
});

const handleChange = (event) => {
setFormData({ ...formData, [event.target.name]: event.target.value });
console.log(formData);
}

const formSubmit = (event) => {
event.preventDefault();
handleSubmit(formData);
};

return (
<section className="m-5">
<h3 className="m-5">Student Form</h3>
<form onSubmit={formSubmit} className="m-5">
<div className="form-group my-2">
<label>Name:-</label>
<input type="text" name="name" className="form-control" placeholder="Enter name" value={formData.name} onChange={handleChange}/>
</div>
<div className="form-group my-2">
<label>Email:-</label>
<input type="text" name="email" className="form-control" placeholder="Enter email" value={formData.email} onChange={handleChange}/>
</div>
<div className="form-group my-2">
<label>Roll Number:-</label>
<input type="text" name="rollnumber" className="form-control" placeholder="Enter Roll Number" value={formData.rollnumber} onChange={handleChange}/>
</div>
<button type="submit" className="btn btn-primary my-2">Submit</button>
</form>
</section>
);
}

Trust me, this is very simple. Firstly I make the student state which is either loaded with data or is empty, depending on if the student prop received is empty or not. Next I define the handleChange() function which runs whenever data is input and this functions updates our state each time we enter a character with our form data. Then I have defined the formSubmit() function and use the handleSubmit() function I recieve as a prop and provide it with the form data.

In the HTML part, I simply define a form element and tell it to go to the formSubmit() function once submitted. Each input field is also given a name that matches the name in the state and also I have specified where to go whenever an input field is changed.

That is it ! Now run your application and check every action and console log messages. This is a very simple CRUD application example with only one model, and can be expanded many ways. I will be linking my GitHub repo down below to give you access to my code.

--

--