How to Integrate a React Application With Rails API
Ruby on Rails is a popular web framework for developing server-side applications. Most of the applications found worldwide are built with Ruby on Rails. Developers find it useful since it provides various tools needed to develop and maintain modern web applications. It is supported by experienced programmers and an active online community that keeps improving it. <!--more--> On the other hand, React is a frontend JavaScript library used to build client-side user interfaces. With React, frontend development is made easy, organized, and efficient since it supports state management, component architecture, and virtual Document Object Model (DOM).
Powerful applications suited for current trends are assured when frontend development is separated from the server-side coding.
In this article, we will utilize React and Ruby on Rails to create a simple to-do list application. We will use React to create the application's frontend that retrieves data in JSON format from the backend, which will run on Rails.
Prerequisites
- Ruby on Rails framework installed
- Node.js and npm package installed
- Yarn package manager installed
- Heroku CLI installed
- A text editor installed preferably VS code
- A web browser installed, preferably Google Chrome
- Proficient in React, Ruby on Rails, JavaScript, Heroku, and Axios
Setting up the backend – The Rails API
First, we will create the backend and browse to the project directory created by running the commands below in the terminal:
$ rails new tdlist-api --api
$ cd tdlist-api
We will proceed to open the project directory in the VS code editor and modify the file Gemfile
by adding the lines below:
gem 'rack-cors', :require => 'rack/cors'
Note, we will use Heroku
to deploy our application later. We will need to add sqllite3
in the Gemfile
development group section for development and testing purposes as shown below:
group :development, :test do
# for development purposes
gem 'sqlite3'
end
If the need arises, we can proceed to add pg
gem to the production group section for production purposes in order to utilize Postgresql
as shown below:
group :production do
# for production purposes
gem 'pg'
end
We will need to generate our model and controller for our application’s backend. We will start by generating the model named Tdlist
by running the command below:
$ rails g model Tdlist title:string done:boolean
Then we proceed to generate our controller named Tdlists
by executing the command below:
$ rails g controller Tdlists index create update destroy
Finally, the command below will generate a table that contains data in the SQLite
database instance named tdlists
:
$ rails db:migrate
Our backend is now set.
Next, in the config/routes.rb
file, specify the new routes for our backend API as shown below:
Rails.application.routes.draw do
scope '/api/version1' do
resources :tdlists
end
end
We have used resources
to utilize POST
, PUT
, GET
, and DELETE
actions in our backend.
As of now, we are dealing with the controller. Next, we are going to utilize the actions we defined above in the code below.
Navigate to the app/controller/tdslist_controller.rb
file and paste the following code:
class TdslistController < ApplicationController
def index
tdlists = Tdlist.order("created_at DESC")
render json: tdlists
end
def create
tdlist = Tdlist.create(tdlist_param)
render json: tdlist
end
def update
tdlist = Tdlist.find(params[:id])
tdlist.update(tdlist_param)
render json: tdlist
end
def destroy
tdlist = Tdlist.find(params[:id])
tdlist.destroy
head :no_content, status: :ok
end
private
def tdlist_param
params.require(:tdlist).permit(:title, :done)
end
end
Next, we will start our server by running the command below in the terminal:
$ rails server
Then, we navigate to the link http://localhost:3000/api/version1/tdlists. A blank page will be displayed since there is no data so far.
In the db/seeds.rb
file, populate some to-do items and check if our API works as expected:
Tdlist.create(title: "Schedule meetings: IT, Accounts, HR", done: false)
Tdlist.create(title: "Visit children's home: perform duties", done: false)
Then we execute the command below:
$ rails db:seed
$ rails server
We can proceed to refresh our page, the following JSON data will be displayed on the browser:
Our backend is now complete. We will configure our frontend to run on port 4000
and the backend to run on port 3000
. Then we will run our application in a local development environment using Heroku CLI
.
To achieve this, we will proceed and create a file named Procfile.windows
in the project root directory and paste the lines of commands below:
api: bundle exec rails server –p 3000
web: yarn --cwd tdlist-app start
The commands above execute both React application and the Rails server on Heroku.
Creating the frontend – React Application
We will begin by creating the React application globally by running the commands below:
$ npm install -g create-react-app
$ create-react-app tdlist-app
In the Rails root directory, a new folder called tdlist-app
containing React files and components will be generated. Then we will execute the command below to start the React application:
$ yarn --cwd tdlist-app start
The default React page will be displayed after running the command above:
Next, we will specify the React application on which port our Rails server is running on in development mode. To achieve this, we will edit the file tdlist-app/package.json
by adding the line of command below:
"proxy": "http://localhost:3000"
The line above instructs the React application to communicate through a proxy in development mode to the backend using port 3000. To call our backend API running on the link http://localhost:3000/api/version1/tdlists from the application, we will only need to call /api/version1/tdlists
instead of the whole link.
We will also need to update our package.json
with the following lines to ensure that our React application runs on port 4000
instead of default port 3000
:
"start": "set PORT=4000 && react-scripts start"
Our updated package.json
will appear as shown below:
We will now call our Procfile.windows
file that we created earlier by running the command below:
$ heroku local -f Procfile.windows
The command above runs the Rail API and the React application. To access it, we can browse to http://localhost:4000.
Creating React components
In this section, we will build the components that our application will use. Navigate to tdlist-app/src/components
directory, create a new file named TdlistsContainer.js
and paste the below code:
import React, { Component } from "react";
class TdlistsContainer extends Component {
render() {
return (
<div>
<div className="taskContainer">
<input
className="newTask"
type="text"
placeholder="Input a New Task and Press Enter"
maxLength="75"
/>
</div>
<div className="wrapItems">
<ul className="listItems"></ul>
</div>
</div>
);
}
}
export default TdlistsContainer;
Then import the new component in App.js
.
import React, { Component } from "react";
import "./App.css";
import TdlistsContainer from "./components/TdlistsContainer";
class App extends Component {
render() {
return (
<div className="mainContainer">
<div className="topHeading">
<h1>A Simple To-Do List App</h1>
</div>
<TdlistsContainer />
</div>
);
}
}
export default App;
Then edit App.css
:
.mainContainer {
width: 35%;
height: 500px;
position: relative;
border-radius: 7px;
margin: 0 auto;
}
.wrapItems {
position: absolute;
bottom: 55px;
top: 170px;
right: 0;
left: 0;
overflow: auto;
}
.topHeading {
color: rgb(48, 2, 2);
font-family: "Times New Roman", Times, serif, sans-serif;
padding: 7px;
text-align: center;
}
ul.listItems {
padding: 0 30px;
}
input.itemCheckbox {
margin-right: 8px;
position: relative;
-webkit-appearance: none;
float: left;
border: 1.5px solid #ccc;
width: 18px;
height: 18px;
border-radius: 7px;
cursor: pointer;
text-align: center;
outline: none;
margin-left: 7px;
font-weight: bold;
}
li.item {
font-family: "Times New Roman", Times, serif;
list-style-type: none;
font-size: 1.5em;
border-bottom: 2px solid rgba(80, 2, 90, 0.411);
padding: 5px 0;
}
li.item:hover .removeItemButton {
opacity: 2;
visibility: visible;
}
input.itemCheckbox:checked:after {
color: green;
width: 15px;
content: "\2713";
font-size: 15px;
display: block;
height: 15px;
left: 0.7px;
position: absolute;
bottom: 1.8px;
}
input.itemCheckbox:checked + label.itemDisplay {
color: #f807a8;
text-decoration: line-through;
}
input.itemCheckbox + label.itemDisplay {
color: black;
}
input[placeholder] {
font-size: 1.2em;
}
.taskContainer {
padding: 15px;
}
.removeItemButton {
float: right;
visibility: hidden;
color: red;
background: rgba(0, 0, 0, 0);
font-size: 25px;
font-weight: bold;
line-height: 0;
border: 1px solid white;
border-radius: 50%;
padding: 10px 5px;
opacity: 0;
margin-right: 7px;
cursor: pointer;
}
.taskContainer .newTask {
padding: 10px;
width: 100%;
box-sizing: border-box;
border-radius: 25px;
}
Next, we will refresh the frontend link: http://localhost:4000, and the following page will be displayed:
Calling the API
In this section, we will fetch the data from our backend. We will use Axios
to fetch or store data. First, we will install Axios
by running the following command and then import it into our component:
$ cd tdlist-app
$ npm install axios --save
Getting to-do list items
We will edit our component to initialize the state and the component’s behavior. We will then add the componentDidMount
function to load the to-do item lists as shown:
import React, { Component } from "react";
import axios from "axios";
class TdlistsContainer extends Component {
constructor(props) {
super(props);
this.state = {
tdlists: [],
};
}
loadTdlists() {
axios
.get("/api/version1/tdlists")
.then((res) => {
this.setState({ tdlists: res.data });
})
.catch((error) => console.log(error));
}
componentDidMount() {
this.loadTdlists();
}
render() {
return (
<div>
<div className="taskContainer">
<input
className="newTask"
type="text"
placeholder="Input a New Task and Press Enter"
maxLength="75"
onKeyPress={this.createTodo}
/>
</div>
<div className="wrapItems">
<ul className="listItems">
{this.state.tdlists.map((tdlist) => {
return (
<li className="item" tdlist={tdlist} key={tdlist.id}>
<input className="itemCheckbox" type="checkbox" />
<label className="itemDisplay">{tdlist.title}</label>
<span className="removeItemButton">x</span>
</li>
);
})}
</ul>
</div>
</div>
);
}
}
export default TdlistsContainer;
Now, we need to restart our Heroku local
and refresh the page, and the results below will be displayed:
Adding a new to-do list item
To add a new to-do list item, we will call POST/api/version1/tdlists
. We need a new function that will enable us to add new to-do list items, and then we proceed to update the state. We will utilize the immutability-helper to update the state. We will run the commands below to install the package, and then we import it into our component:
$ npm install immutability-helper --save
import update from "immutability-helper";
We will then update the textbox attributes to have an onKeyPress
event as below:
<input
className="newTask"
type="text"
placeholder="Input a New Task and Press Enter"
maxLength="75"
onKeyPress={this.newTdlist}
/>
Then, we create a newTdlist
function as below:
newTdlist = (e) => {
if (e.key === "Enter" && !(e.target.value === "")) {
axios
.post("/api/version1/tdlists", { tdlist: { title: e.target.value } })
.then((res) => {
const tdlists = update(this.state.tdlists, {
$splice: [[0, 0, res.data]],
});
this.setState({
tdlists: tdlists,
inputValue: "",
});
})
.catch((error) => console.log(error));
}
};
In the code snippet above, we added a new to-do list item at the top of the tdlists
array through the use of $splice
function. We can then proceed to add a new to-do list item to test the application.
We can note that after adding a new to-do list item, the value of textbox remains the same. To clear the textbox, we add the following code:
this.state = {
tdlists: [],
inputValue: "",
};
To update the state with the new parameter we defined in the code snippet above, we paste the code below:
this.setState({
tdlists: tdlists,
inputValue: "",
});
We will proceed to add a new function that is invoked whenever the value of the textbox changes:
handleChange = (e) => {
this.setState({ inputValue: e.target.value });
};
Finally, we will edit the textbox to have new attributes:
<input
className="newTask"
type="text"
placeholder="Input a New Task and Press Enter"
maxLength="75"
onKeyPress={this.newTdlist}
value={this.state.inputValue}
onChange={this.handleChange}
/>
Updating to-do list items
To mark the to-do list as done, we will modify the checkbox element and create a new function that updates the state as below:
<input
className="itemCheckbox"
type="checkbox"
checked={tdlist.done}
onChange={(e) => this.modifyTdlist(e, tdlist.id)}
/>
modifyTdlist = (e, id) => {
axios
.put(`/api/version1/tdlists/${id}`, { tdlist: { done: e.target.checked } })
.then((res) => {
const tdlistIndex = this.state.tdlists.findIndex(
(x) => x.id === res.data.id
);
const tdlists = update(this.state.tdlists, {
[tdlistIndex]: { $set: res.data },
});
this.setState({
tdlists: tdlists,
});
})
.catch((error) => console.log(error));
};
Deleting to-do list items
To delete a to-do list item, we will update the span
element as seen below:
<span
className="removeItemButton"
onClick={(e) => this.removeTdlist(tdlist.id)}
>
x
</span>
We will then create a removeTdlist
function:
removeTdlist = (id) => {
axios
.delete(`/api/version1/tdlists/${id}`)
.then((res) => {
const tdlistIndex = this.state.tdlists.findIndex((x) => x.id === id);
const tdlists = update(this.state.tdlists, {
$splice: [[tdlistIndex, 1]],
});
this.setState({
tdlists: tdlists,
});
})
.catch((error) => console.log(error));
};
The application is now ready for testing.
Wrapping up
We have successfully implemented a React application and created the component, TdlistsContainer
. We have handled the behavior of our application using the React state.
On the other hand, with Rails, we have built a backend that handles the JSON data as we deal with our frontend. We used the main container to render the User Interface instead of using components to make things simple. This gave us more time to focus on other significant concepts and data flow.
The code used in this tutorial can be found on GitHub.
Hope you find this tutorial helpful.
Happy coding!
Peer Review Contributions by: Monica Masae