Simple Rails CRUD app with React Frontend, using ‘react-rails’ gem

Lucya Koroleva
Quick Code
Published in
11 min readFeb 20, 2018

During my bootcamp with Flatiron School I learned how to create apps that use Rails API backend and React frontend, which is only one of the possible ways for using these two technologies together. I decided to investigate a different scenario, in which developers utilise gem ‘react-rails’ in order to create and manage React Components directly inside Rails javascript asset pipeline.

One of best articles I found was one by Hristo Georgiev: https://www.pluralsight.com/guides/ruby-ruby-on-rails/building-a-crud-interface-with-react-and-ruby-on-rails. However after working though it I noticed that it used mainly old React syntax, as well as outdated use of ‘refs’. The goal of this blog is to present a similar tutorial for building very basic CRUD app, but with a slightly refreshed React syntax.

Creating new Rails app with one model

We are going to start by creating a new Rails app, running the following command:

rails new fruits-app

Next head to Gemfile and add gem 'react-rails', next run

bundle install

then

rails g react:install

React:install generator that we are using here automatically adds React JS library to your asset pipeline.

The model that we are going to be working with is Fruit, so let’s go ahead and generate it:

rails g model Fruit name:string description:text

Jump into your db/seeds.rb file and create some sample items:

fruits = ['Mango', 'Pineapple', 'Passion fruit', 'Dragonfruit']fruits.each{|fruit| Fruit.create(name: fruit, description: "I am a delicious #{fruit}.")}

Migrate and seed your database:

rake db:migrate db:seed

Controllers

We need to make only one adjustment to our Application Controller. So inside app/controllers/application_controller.rb paste the following line of code:

class ApplicationController < ActionController::Base
protect_from_forgery with: :null_session
end

Our application is API-based and so controllers folders structure require us following namespace convention with API version specification, like so: app/controllers/api/v1. Versioning the API means that changes can be made to it in future without damaging the original version.

So let’s create these two descending folders (api and v1) inside app/controllers, and a new file inside them calledfruits_controller.rb

Inside our Fruits controller let’s define CRUD actions.

class Api::V1::FruitsController < ApplicationController
def index
render json: Fruit.all
end

def create
fruit = Fruit.create(fruit_params)
render json: fruit
end

def destroy
Fruit.destroy(params[:id])
end

def update
fruit = Fruit.find(params[:id])
fruit.update_attributes(fruit_params)
render json: fruit
end

private

def fruit_params
params.require(:fruit).permit(:id, :name, :description)
end
end

Routing the controllers

Let’s navigate config/routes.rb and set up all the necessary CRUD routes:

Rails.application.routes.draw do 
namespace :api do
namespace :v1 do
resources :fruits, only: [:index, :create, :destroy, :update]
end
end
end

At this point if you navigate to http://localhost:3000/api/v1/fruits.json in your browser, you should see JSON with our 4 fruits. Cool!

Now let’s create one more controller called Home in a corresponding new file app/controllers/home_controller.rb that will be responsible for just one index page, where we are going to perform all of our React magic.

class HomeController < ApplicationController
def index
end
end

Let’s return to config/routes.rb for a second and add our Home index page to be the root:

root to: 'home#index'

Now all our index page need is a corresponding view. Create a new ‘home’ folder inside app/views and add index.html.erb file to it:

app/views/home/index.html.erb

This file will only contain one line that takes advantage of react_component view helper that comes with ‘react-rails’ gem. It’s not going to work right now, but it all will make sense in a moment.

<%= react_component 'Main' %>

Creating our first React Component

Create a file named app/assets/javascripts/components/_main.js.jsx

We are going to follow the usual syntax for creating a stateless/presentational React Component that we will call Main:

const Main = (props) => {
return(
<div>
<h1>Fruits are great!</h1>
</div>
)
}

Head to localhost:3000 in the browser and see for yourself that our first Component was loaded on the page. Pretty cool!

Displaying all Fruits

Let’s create our next component that will display the list of fruits. Create a new file named app/assets/javascripts/components/_all_fruits.js.jsx and define a new component called AllFruits:

class AllFruits extends React.Component {render(){
return(
<div>
<h1>To do: List of fruits</h1>
</div>
)
}
}

Next let’s place this component inside our Main component and make sure it loads to the page successfully.

const Main = (props) => {
return(
<div>
<h1>Fruits are great!</h1>
<AllFruits />
</div>
)
}

The next step is creating a state for AllFruits component, as well as actually fetching all fruits from our API, and finally using this.setState() to update the empty state created in the constructor. We are going to add both the constructor and componentDidMount() lifecycle hook that handles fetch request in our next step:

class AllFruits extends React.Component {

constructor(props) {
super(props);
this.state = {
fruits: []
};
}
componentDidMount(){
fetch('/api/v1/fruits.json')
.then((response) => {return response.json()})
.then((data) => {this.setState({ fruits: data }) });
}
render(){
return(
<div>
<h1>To do: List of fruits</h1>
</div>
)
}
}

And finally we are going to update render() method for AllFruits component to iterate over each fruit in the state, displaying name and description for each item.

render(){
var fruits = this.state.fruits.map((fruit) => {
return(
<div key={fruit.id}>
<h1>{fruit.name}</h1>
<p>{fruit.description}</p>
</div>
)
})
return(
<div>
{fruits}
</div>
)
}

You will notice that we used an additional key attribute that we set equal to fruit.id. That is our way to later refer to a particular item in the list of similar items, and a requirement in React. It is highly recommended to learn React in 2021. Head to localhost:3000 to see our initial fruit list!

Adding a new fruit

Before we create our newFruit component and form let’s pause here and modify our app structure a little bit. We want to have one main component that will handle all changes of fruits state. Let’s create one more higher level container component called Body, and move constructor with state, as well as componentDidMount() fetch portion of code from AllFruits to Body. Create a new file /app/assets/javascripts/components/_body.js.jsx

class Body extends React.Component {constructor(props) {
super(props);
this.state = {
fruits: []
};
}
componentDidMount(){
fetch('/api/v1/fruits.json')
.then((response) => {return response.json()})
.then((data) => {this.setState({ fruits: data }) });
}
render(){
return(
<div>
<AllFruits fruits={this.state.fruits} />
</div>
)
}
}

Updated AllFruits component can be modified to be stateless/presentational:

const AllFruits = (props) => {var fruits = props.fruits.map((fruit) => {
return(
<div key={fruit.id}>
<h1>{fruit.name}</h1>
<p>{fruit.description}</p>
</div>
)
})
return(
<div>
{fruits}
</div>
)
}

Updated Main component:

const Main = (props) => {
return(
<div>
<h1>Fruits are great!</h1>
<Body />
</div>
)
}

We are rendering AllFruits component now from inside this higher level Body component and thus passing fruits to it as a prop. Check your browser to see if you followed everything correctly up to this point and our fruit list is displayed.

Now we can add a form for creating a new fruit to the page. Create a new file /app/assets/javascripts/components/_new_item.js.jsx and define a new component NewFruit with a simple form containing two fields:

const NewFruit = (props) => {  let formFields = {}

return(
<form>
<input ref={input => formFields.name = input} placeholder='Enter the name of the item'/>
<input ref={input => formFields.description = input} placeholder='Enter a description' />
<button>Submit</button>

</form>
)
}

Did you notice those ref = {input => formFields.name = input} ? Similarly to keys in our iterations, we use refs to have access to a certain DOM element. In this case it will allow us to grab the value of our inputs. In case of our app i’m storing form inputs in an object formFields so we can pass fields values to the parent component later on using our onClick() event handler that we will place inside button.

class Body extends React.Component {constructor(props) {
...
}
componentDidMount(){
...
}
render(){
return(
<div>
<NewFruit />
<AllFruits fruits={this.state.fruits} />
</div>
)
}
}

Place NewFruit component into Body and you will see our newly made form appear in the browser.

Next step is to actually make our Submit button work.

Inside Body component we will create handleFormSubmit() function that receives name and description from the form as arguments. For now it will just console.log() arguments for us to see if it all works. We will also pass this handleFormSubmit() function to NewFruit component as a prop.

class Body extends React.Component {constructor(props) {
...
this.handleFormSubmit = this.handleFormSubmit.bind(this)
}
handleFormSubmit(name, description){
console.log(name, description)
}
componentDidMount(){
...
}
render(){
return(
<div>
<NewFruit handleFormSubmit={this.handleFormSubmit}/>
<AllFruits fruits={this.state.fruits} />
</div>
)
}
}

As for our NewFruit component all we have to do is add onSubmit event handler to our form tag that will pass values from fields as props. I am also passing our event (e) as an argument in order to clear the fields after form submission:

const NewFruit = (props) => {

let formFields = {}

return(
<form onSubmit={ (e) => {
e.preventDefault();
props.handleFormSubmit(
formFields.name.value,
formFields.description.value
);
e.target.reset();}
}
>
<input ref={input => formFields.name = input} placeholder='Enter the name of the item'/>
<input ref={input => formFields.description = input} placeholder='Enter a description' />
<button>Submit</button>
</form>
)
}

Let’s head back to Body and actually handle the whole creation of the new fruit.

class Body extends React.Component {  constructor(props) {
super(props);
this.state = {
fruits: []
};
this.handleFormSubmit = this.handleFormSubmit.bind(this)
this.addNewFruit = this.addNewFruit.bind(this)

}
handleFormSubmit(name, description){
let body = JSON.stringify({fruit: {name: name, description: description} })
fetch('http://localhost:3000/api/v1/fruits', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: body,
}).then((response) => {return response.json()})
.then((fruit)=>{
this.addNewFruit(fruit)
})

}
addNewFruit(fruit){
this.setState({
fruits: this.state.fruits.concat(fruit)
})
}
componentDidMount(){
fetch('/api/v1/fruits.json')
.then((response) => {return response.json()})
.then((data) => {this.setState({ fruits: data }) });
}
render(){
return(
<div>
<NewFruit handleFormSubmit={this.handleFormSubmit}/>
<AllFruits fruits={this.state.fruits} />
</div>
)
}
}

A lot of things just happened up there. Inside handleFormSubmit() we are now using fetch() to send a POST request to the backend with our new fruit data. As soon as promise is returned we call new function handleAddNewFruit() that takes returned fruit object as an argument and updates state using this.setState() to load this new fruit to the page immediately. Pretty neat! Now you can head to the browser and see for yourself that it works.

Deleting a fruit

Next step on the way to implementing full CRUD functionality for our Fruit model is handling fruit deletion.

So far we have been loading all fruits as a list inside AllFruits. It is a good practice however to make Fruit a separate entity and move it to a separate Fruit component. Let’s do that!

Create a new file app/assets/javascripts/components/_fruit.js.jsx. Here is how our Fruit component is going to look for now:

class Fruit extends React.Component{

render(){
return(
<div>
<h1>{this.props.fruit.name}</h1>
<p>{this.props.fruit.description}</p>
</div>
)
}
}

As you see we are loading name and description using this.props.fruit. It means that we will pass whole fruit object as a prop from our parent AllItems Component, like so:

const AllFruits = (props) => {var fruits = props.fruits.map((fruit) => {
return(
<div key={fruit.id}>
<Fruit fruit={fruit}/>
</div>
)
})
return(
<div>
{fruits}
</div>
)
}

Check the browser again to see if you implemented all the changes correctly. The page should look exactly the same as before, and all fruits should be displayed along with our new fruit form.

Now let’s add Delete button to Fruit.

class Fruit extends React.Component{

render(){
return(
<div>
<h1>{this.props.fruit.name}</h1>
<p>{this.props.fruit.description}</p>
<button onClick={() => this.props.handleDelete()}>Delete</button>
</div>
)
}
}

We didn’t create handleDelete() function yet, but it will be passed as a prop from Body to AllItems and then to Fruit Component. Let’s go ahead and define that function in Body Component, bind it in constructor, and pass as a prop to AllFruits:

class Body extends React.Component {constructor(props) {
...
this.handleDelete = this.handleDelete.bind(this)
}
handleDelete(id){
fetch(`
http://localhost:3000/api/v1/fruits/${id}`,
{
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
}).then((response) => {
console.log('Item was deleted!')
})
}
render(){
return(
<div>
<NewFruit handleFormSubmit={this.handleFormSubmit}/>
<AllFruits fruits={this.state.fruits} handleDelete={this.handleDelete}/>
</div>
)
}
}

The way our handleDelete() works is the following: it receives fruit’s id, sends a DELETE request to the fruit route with that id using fetch() and (just for now) console.logs ‘Item was deleted!’.

Our next steps:

  1. Make sure that AllFruits passes handleDelete as a prop to Fruit Component.
const AllFruits = (props) => {var fruits = props.fruits.map((fruit) => {
return(
<div key={fruit.id}>
<Fruit fruit={fruit} handleDelete={props.handleDelete}/>
</div>
)
})
...
}

2. Pass fruit’s id from Fruit component.

class Fruit extends React.Component{

render(){
return(
<div>
<h1>{this.props.fruit.name}</h1>
<p>{this.props.fruit.description}</p>
<button onClick={() => this.props.handleDelete(this.props.fruit.id)}>Delete</button>
</div>
)
}
}

3. Create a function that handles deleting item from the state, so it is immediately removed from the page without a refresh.

class Body extends React.Component {constructor(props) {
...
this.handleDelete = this.handleDelete.bind(this)
this.deleteFruit = this.deleteFruit.bind(this)
}
...handleDelete(id){
fetch(`http://localhost:3000/api/v1/items/${id}`,
{
method: 'DELETE',
headers: {
'Content-Type': 'application/json'
}
}).then((response) => {
this.deleteFruit(id)
})
}
deleteFruit(id){
newFruits = this.state.fruits.filter((fruit) => fruit.id !== id)
this.setState({
fruits: newFruits
})
}
componentDidMount(){
...
}
render(){
return(
<div>
<NewFruit handleFormSubmit={this.handleFormSubmit}/>
<AllFruits fruits={this.state.fruits} handleDelete={this.handleDelete}/>
</div>
)
}
}

Also, apart from React, the ruby on rails tutorial can help you explore the app development process more freely. That’s it! Head to the browser and delete all the fruits that you hate!

Editing a fruit

All we have left to build to finish our CRUD functionality is an ability to edit/update a fruit. Let’s do so.

Let’s modify our Fruit component to create a state with ‘editable’ attribute and also to add an Edit button.

class Fruit extends React.Component{constructor(props){
super(props);
this.state = {
editable: false
}
}


render(){
return(
<div>
<h1>{this.props.fruit.name}</h1>
<p>{this.props.fruit.description}</p>
<button>{this.state.editable? 'Submit' : 'Edit'}</button>
<button onClick={() => this.props.handleDelete(this.props.fruit.id)}>Delete</button>
</div>
)
}
}

We will next modify the way we render Fruit. Depending on this.state.editable value it will render name and description OR input fields to edit this fruit.

class Fruit extends React.Component{constructor(props){
super(props);
this.state = {
editable: false
}
}

render(){
let name = this.state.editable ? <input type='text' ref={input => this.name = input} defaultValue={this.props.fruit.name}/>:<h3>{this.props.fruit.name}</h3>
let description = this.state.editable ? <input type='text' ref={input => this.description = input} defaultValue={this.props.fruit.description}/>:<p>{this.props.fruit.description}</p>

return(
<div>
{name}
{description}
<button>{this.state.editable? 'Submit' : 'Edit'}</button>
<button onClick={() => this.props.handleDelete(this.props.fruit.id)}>Delete</button>
</div>
)
}
}

Next let’s add an onClick() event handler to the Edit button, and create the actual function that handles an edit:

class Fruit extends React.Component{constructor(props){
super(props);
this.state = {
editable: false
}
this.handleEdit = this.handleEdit.bind(this)
}
handleEdit(){
this.setState({
editable: !this.state.editable
})
}


render(){
let name = this.state.editable ? <input type='text' ref={input => this.name = input} defaultValue={this.props.fruit.name}/>:<h3>{this.props.fruit.name}</h3>
let description = this.state.editable ? <input type='text' ref={input => this.description = input} defaultValue={this.props.fruit.description}/>:<p>{this.props.fruit.description}</p>
return(
<div>
{name}
{description}
<button onClick={() => this.handleEdit()}>{this.state.editable? 'Submit' : 'Edit'}</button>
<button onClick={() => this.props.handleDelete(this.props.fruit.id)}>Delete</button>
</div>
)
}
}

For now all this button does is changes our editable attribute on click.

Just as with Delete action, the actual update action along with PUT request to the back end will happen in the higher level component Body. So let’s create functions that will send PUT request and will update fruits state accordingly.

class Body extends React.Component {  constructor(props) {
...
this.handleUpdate = this.handleUpdate.bind(this);
this.updateFruit = this.updateFruit.bind(this)

}
handleUpdate(fruit){
fetch(`
http://localhost:3000/api/v1/fruits/${fruit.id}`,
{
method: 'PUT',
body: JSON.stringify({fruit: fruit}),
headers: {
'Content-Type': 'application/json'
}
}).then((response) => {
this.updateFruit(fruit)
})
}
updateFruit(fruit){
let newFruits = this.state.fruits.filter((f) => f.id !== fruit.id)
newFruits.push(fruit)
this.setState({
fruits: newFruits
})
}

componentDidMount(){
fetch('/api/v1/items.json')
.then((response) => {return response.json()})
.then((data) => {this.setState({ items: data }) });
}
render(){
return(
<div>
<NewItem handleSubmit = {this.handleSubmit} />
<AllItems items={this.state.items} handleDelete = {this.handleDelete} handleUpdate = {this.handleUpdate}/>
</div>
)
}
}

Once again we defined two functions: handleUpdate() that takes fruit as an argument and sends PUT request to the backend, and updateFruit() that seamlessly updates fruit with updated info in the browser. handleUpdate() is passed to AllItems as a prop, and AllItems passes it as a prop to Fruit:

const AllFruits = (props) => {var fruits = props.fruits.map((fruit) => {
return(
<div key={fruit.id}>
<Fruit fruit={fruit} handleDelete={props.handleDelete} handleUpdate={props.handleUpdate}/>
</div>
)
})
...
}

Almost there!!! All that’s left is create and pass a proper fruit object from Fruit Component to handleUpdate function that is called inside Fruit’s handleEdit function.

class Fruit extends React.Component{constructor(props){
...
this.handleEdit = this.handleEdit.bind(this)
}
handleEdit(){
if(this.state.editable){
let name = this.name.value
let description = this.description.value
let id = this.props.fruit.id
let fruit = {id: id, name: name, description: description}
this.props.handleUpdate(fruit)
}

this.setState({
editable: !this.state.editable
})
}

render(){
let name = this.state.editable ? <input type='text' ref={input => this.name = input} defaultValue={this.props.fruit.name}/>:<h3>{this.props.fruit.name}</h3>
let description = this.state.editable ? <input type='text' ref={input => this.description = input} defaultValue={this.props.fruit.description}/>:<p>{this.props.fruit.description}</p>
return(
<div>
{name}
{description}
<button onClick={() => this.handleEdit()}>{this.state.editable? 'Submit' : 'Edit'}</button>
<button onClick={() => this.props.handleDelete(this.props.fruit.id)}>Delete</button>
</div>
)
}
}

That’s it! Now You can perform all CRUD actions with your fruits. There was a lot of code in this blog post and we made a ton of changes to it at each step, so if you’d like to see the full code of the finished app, please head here:

https://github.com/nothingisfunny/fruits-crud-react-rails-app

Thanks for sticking around and please leave comments if you have any suggestions on how to improve this app, or if you noticed any mistakes/typos!

--

--