How to Build a React App that Works with a Rails 5.1 API

    Hrishi Mittal
    Share

    React + Ruby on Rails = 🔥

    React has taken the frontend development world by storm. It’s an excellent JavaScript library for building user interfaces. And it’s great in combination with Ruby on Rails. You can use Rails on the back end with React on the front end in various ways.

    In this hands-on tutorial, we’re going to build a React app that works with a Rails 5.1 API.

    You can watch a video version of this tutorial here.

    Watch a video version of this tutorial

    To follow this tutorial, you need to be comfortable with Rails and know the basics of React.

    If you don’t use Rails, you can also build the API in the language or framework of your choice, and just use this tutorial for the React part.

    The tutorial covers stateless functional components, class-based components, using Create React App, use of axios for making API calls, immutability-helper and more.

    What We’re Going to Build

    We’re going to build an idea board as a single page app (SPA), which displays ideas in the form of square tiles.

    You can add new ideas, edit them and delete them. Ideas get auto-saved when the user focuses out of the editing form.

    A demo of the Idea board app

    At the end of this tutorial, we’ll have a functional CRUD app, to which we can add some enhancements, such as animations, sorting and search in a future tutorial.

    You can see the full code for the app on GitHub:

    Ideaboard Rails API

    Ideaboard React frontend

    Setting up the Rails API

    Let’s get started by building the Rails API. We’ll use the in-built feature of Rails for building API-only apps.

    Make sure you have version 5.1 or higher of the Rails gem installed.

    gem install rails -v 5.1.3
    

    At the time of writing this tutorial, 5.1.3 is the latest stable release, so that’s what we’ll use.

    Then generate a new Rails API app with the --api flag.

    rails new --api ideaboard-api
    cd ideaboard-api
    

    Next, let’s create the data model. We only need one data model for ideas with two fields — a title and a body, both of type string.

    Let’s generate and run the migration:

    rails generate model Idea title:string body:string
    
    rails db:migrate
    

    Now that we’ve created an ideas table in our database, let’s seed it with some records so that we have some ideas to display.

    In the db/seeds.rb file, add the following code:

    ideas = Idea.create(
      [
        {
          title: "A new cake recipe",
          body: "Made of chocolate"
        },
        {
          title: "A twitter client idea",
          body: "Only for replying to mentions and DMs"
        },
        {
          title: "A novel set in Italy",
          body: "A mafia crime drama starring Berlusconi"
        },
        {
          title: "Card game design",
          body: "Like Uno but involves drinking"
        }
      ])
    

    Feel free to add your own ideas.

    Then run:

    rails db:seed
    

    Next, let’s create an IdeasController with an index action in app/controllers/api/v1/ideas_controller.rb:

    module Api::V1
      class IdeasController < ApplicationController
        def index
          @ideas = Idea.all
          render json: @ideas
        end
      end
    end
    

    Note that the controller is under app/controllers/api/v1 because we’re versioning our API. This is a good practice to avoid breaking changes and provide some backwards compatibility with our API.

    Then add ideas as a resource in config/routes.rb:

    Rails.application.routes.draw do
      namespace :api do
        namespace :v1 do
          resources :ideas  
        end
      end
    end
    

    Alright, now let’s test our first API endpoint!

    First, let’s start the Rails API server on port 3001:

    rails s -p 3001
    

    Then, let’s test our endpoint for getting all ideas with curl:

    curl -G http://localhost:3001/api/v1/ideas
    

    And that prints all our ideas in JSON format:

    [{"id":18,"title":"Card game design","body":"Like Uno but involves drinking","created_at":"2017-09-05T15:42:36.217Z","updated_at":"2017-09-05T15:42:36.217Z"},{"id":17,"title":"A novel set in Italy","body":"A mafia crime drama starring Berlusconi","created_at":"2017-09-05T15:42:36.213Z","updated_at":"2017-09-05T15:42:36.213Z"},{"id":16,"title":"A twitter client idea","body":"Only for replying to mentions and DMs","created_at":"2017-09-05T15:42:36.209Z","updated_at":"2017-09-05T15:42:36.209Z"},{"id":15,"title":"A new cake recipe","body":"Made of chocolate","created_at":"2017-09-05T15:42:36.205Z","updated_at":"2017-09-05T15:42:36.205Z"}]
    

    We can also test the endpoint in a browser by going to http://localhost:3001/api/v1/ideas.

    Testing our API endpoint in a browser

    Setting up Our Front-end App Using Create React App

    Now that we have a basic API, let’s set up our front-end React app using Create React App. Create React App is a project by Facebook that helps you get started with a React app quickly without any configuration.

    First, make sure you have Node.js and npm installed. You can download the installer from the Node.js website. Then install Create React App by running:

    npm install -g create-react-app
    

    Then, make sure you’re outside the Rails directory and run the following command:

    create-react-app ideaboard
    

    That will generate a React app called ideaboard, which we’ll now use to talk to our Rails API.

    Let’s run the React app:

    cd ideaboard
    npm start
    

    This will open it on http://localhost:3000.

    Homepage of a new app generated by Create React App

    The app has a default page with a React component called App that displays the React logo and a welcome message.

    The content on the page is rendered through a React component in the src/App.js file:

    import React, { Component } from 'react'
    import logo from './logo.svg'
    import './App.css'
    
    class App extends Component {
      render() {
        return (
          <div className="App">
            <div className="App-header">
              <img src={logo} className="App-logo" alt="logo" />
              <h2>Welcome to React</h2>
            </div>
            <p className="App-intro">
              To get started, edit <code>src/App.js</code> and save to reload.
            </p>
          </div>
        );
      }
    }
    
    export default App
    

    Our First React Component

    Our next step is to edit this file to use the API we just created and list all the ideas on the page.

    Let’s start off by replacing the Welcome message with an h1 tag with the title of our app ‘Idea Board’.

    Let’s also add a new component called IdeasContainer. We need to import it and add it to the render function:

    import React, { Component } from 'react'
    import './App.css'
    import IdeasContainer from './components/IdeasContainer'
    
    class App extends Component {
      render() {
        return (
          <div className="App">
            <div className="App-header">
              <h1>Idea Board</h1>
            </div>
            <IdeasContainer />
          </div>
        );
      }
    }
    
    export default App
    

    Let’s create this IdeasContainer component in a new file in src/IdeasContainer.js under a src/components directory.

    import React, { Component } from 'react'
    
    class IdeasContainer extends Component {
      render() {
        return (
          <div>
            Ideas
          </div>
        )
      }
    }
    
    export default IdeasContainer
    

    Let’s also change the styles in App.css to have a white header and black text, and also remove styles we don’t need:

    .App-header {
      text-align: center;
      height: 150px;
      padding: 20px;
    }
    
    .App-intro {
      font-size: large;
    }
    

    Skeleton Idea board app

    This component needs to talk to our Rails API endpoint for getting all ideas and display them.

    Fetching API Data with axios

    We’ll make an Ajax call to the API in the componentDidMount() lifecycle method of the IdeasContainer component and store the ideas in the component state.

    Let’s start by initializing the state in the constructor with ideas as an empty array:

    constructor(props) {
      super(props)
      this.state = {
        ideas: []
      }
    }
    

    And then we’ll update the state in componentDidMount().

    Let’s use the axios library for making the API calls. You can also use fetch or jQuery if you prefer those.

    Install axios with npm:

    npm install axios --save
    

    Then import it in IdeasContainer:

    import axios from 'axios'
    

    And use it in componentDidMount():

    componentDidMount() {
      axios.get('http://localhost:3001/api/v1/ideas.json')
      .then(response => {
        console.log(response)
        this.setState({ideas: response.data})
      })
      .catch(error => console.log(error))
    }
    

    Now if we refresh the page … it won’t work!

    No Access-Control-Allow-Origin header present

    We’ll get a “No Access-Control-Allow-Origin header present” error, because our API is on a different port and we haven’t enabled Cross Origin Resource Sharing (CORS).

    Enabling Cross Origin Resource Sharing (CORS)

    So let’s first enable CORS using the rack-cors gem in our Rails app.

    Add the gem to the Gemfile:

    gem 'rack-cors', :require => 'rack/cors'
    

    Install it:

    bundle install
    

    Then add the middleware configuration to config/application.rb file:

    config.middleware.insert_before 0, Rack::Cors do
      allow do
        origins 'http://localhost:3000'
        resource '*', :headers => :any, :methods => [:get, :post, :put, :delete, :options]
      end
    end
    

    We restrict the origins to our front-end app at http://localhost:3000 and allow access to the standard REST API endpoint methods for all resources.

    Now we need to restart the Rails server, and if we refresh the browser, we’ll no longer get the CORS error.

    The page will load fine and we can see the response data logged in the console.

    Ideas JSON response from API logged to the console

    So now that we know we’re able to fetch ideas from our API, let’s use them in our React component.

    We can change the render function to iterate through the list ideas from the state and display each of them:

    render() {
      return (
        <div>
          {this.state.ideas.map((idea) => {
            return(
              <div className="tile" key={idea.id} >
                <h4>{idea.title}</h4>
                <p>{idea.body}</p>
              </div>
            )       
          })}
        </div>
      );
    }
    

    That will display all the ideas on the page now.

    List of ideas displayed by component

    Note the key attribute on the tile div.

    We need to include it when creating lists of elements. Keys help React identify which items have changed, are added, or are removed.

    Now let’s add some styling in App.css to make each idea look like a tile:

    .tile {
      height: 150px;
      width: 150px;
      margin: 10px;
      background: lightyellow;
      float: left;
      font-size: 11px;
      text-align: left;
    }
    

    We set the height, width, background color and make the tiles float left.

    Styled idea tiles

    Stateless functional components

    Before we proceed, let’s refactor our code so far and move the JSX for the idea tiles into a separate component called Idea.

    import React from 'react'
    
    const Idea = ({idea}) =>
      <div className="tile" key={idea.id}>
        <h4>{idea.title}</h4>
        <p>{idea.body}</p>
      </div>
    
    export default Idea
    

    This is a stateless functional component (or as some call it, a “dumb” component), which means that it doesn’t handle any state. It’s a pure function that accepts some data and returns JSX.

    Then inside the map function in IdeasContainer, we can return the new Idea component:

    {this.state.ideas.map((idea) => {
      return (<Idea idea={idea} key={idea.id} />)
    })}
    

    Don’t forget to import Idea as well:

    import Idea from './Idea'
    

    Great, so that’s the first part of our app complete. We have an API with an endpoint for getting ideas and a React app for displaying them as tiles on a board!

    Adding a new record

    Next, we’ll add a way to create new ideas.

    Let’s start by adding a button to add a new idea.

    Inside the render function in IdeasContainer, add:

    <button className="newIdeaButton">
      New Idea
    </button>
    

    And let’s add some styling for it in App.css:

    .newIdeaButton {
      background: darkblue;
      color: white;
      border: none;
      font-size: 18px;
      cursor: pointer;
      margin-right: 10px;
      margin-left: 10px;
      padding:10px;
    }
    

    New Idea button

    Now when we click the button, we want another tile to appear with a form to edit the idea.

    Once we edit the form, we want to submit it to our API to create a new idea.

    API endpoint for creating a new idea

    So let’s start of by first making an API endpoint for creating new ideas in IdeasController:

    def create
      @idea = Idea.create(idea_params)
      render json: @idea
    end
    
    private
    
      def idea_params
        params.require(:idea).permit(:title, :body)
      end
    

    Since Rails uses strong parameters, we define the private method idea_params to whitelist the params we need — title and body.

    Now we have an API endpoint to which we can post idea data and create new ideas.

    Back in our React app, now let’s add a click handler called addNewIdea to the new idea button:

    <button className="newIdeaButton"
      onClick={this.addNewIdea} >
      New Idea
    </button>
    

    Let’s define addNewIdea as a function that uses axios to make a POST call to our new idea endpoint with a blank idea. Let’s just log the response to the console for now:

    addNewIdea = () => {
      axios.post(
        'http://localhost:3001/api/v1/ideas',
        { idea:
          {
            title: '',
            body: ''
          }
        }
      )
      .then(response => {
        console.log(response)
      })
      .catch(error => console.log(error))
    }
    

    Now if we try clicking on the new idea button in the browser, we’ll see in the console that the response contains a data object with our new idea with a blank title and body.

    Blank idea data logged to the console

    When we refresh the page, we can see an empty tile representing our new idea.

    Blank idea tile

    What we really want to happen is that, when we click the new idea button, an idea is created immediately, and a form for editing that idea appears on the page.

    This way, we can use the same form and logic for editing any idea later on in the tutorial.

    Before we do that, let’s first order the ideas on the page in reverse chronological order so that the newest ideas appear at the top.

    So let’s change the definition of @ideas in IdeasController to order ideas in descending order of their created_at time:

    module Api::V1
      class IdeasController < ApplicationController
        def index
          @ideas = Idea.order("created_at DESC")
          render json: @ideas
        end
      end
    end
    

    Alright, now the latest ideas are displayed first.

    Display newest ideas first

    Now, let’s continue with defining addNewIdea.

    First, let’s use the response from our POST call to update the array of ideas in the state, so that when we add a new idea it appears on the page immediately.

    We could just push the new idea to the array, since this is only an example app, but it’s good practice to use immutable data.

    So let’s use immutability-helper, which is a nice package for updating data without directly mutating it.

    Install it with npm:

    npm install immutability-helper --save
    

    Then import the update function in IdeasContainer:

    import update from 'immutability-helper'
    

    Now let’s use it inside addNewIdea to insert our new idea at the beginning of the array of ideas:

    addNewIdea = () => {
      axios.post(
        'http://localhost:3001/api/v1/ideas',
        { idea:
          {
            title: '',
            body: ''
          }
        }
      )
      .then(response => {
        console.log(response)
        const ideas = update(this.state.ideas, {
          $splice: [[0, 0, response.data]]
        })
        this.setState({ideas: ideas})
      })
      .catch(error => console.log(error))
    }
    

    We make a new copy of this.state.ideas and use the $splice command to insert the new idea (in response.data) at the 0th index of this array.

    Then we use this new ideas array to update the state using setState.

    Now if we try the app in the browser and click the new idea button, a new empty tile appears immediately.

    Add a new idea

    Now we can proceed with editing this idea.

    First, we need a new state property editingIdeaId, which keeps track of which idea is being currently edited.

    By default, we’re not editing any idea, so let’s initialize editingIdeaId in the state with a null value:

    this.state = {
      ideas: [],
      editingIdeaId: null
    }
    

    Now when we add a new idea, in addition to adding it to state.ideas, we also want to set its id as the value of state.editingIdeaId. So let’s modify the setState call in addNewIdea to include also set editingIdeaId:

    this.setState({
      ideas: ideas,
      editingIdeaId: response.data.id
    })
    

    So this indicates that we’ve just added a new idea and we want to edit it immediately.

    The complete addNewIdea function now looks like this:

    addNewIdea = () => {
      axios.post(
        'http://localhost:3001/api/v1/ideas',
        { idea:
          {
            title: '',
            body: ''
          }
        }
      )
      .then(response => {
        const ideas = update(this.state.ideas, {
          $splice: [[0, 0, response.data]]
        })
        this.setState({
          ideas: ideas,
          editingIdeaId: response.data.id
        })
      })
      .catch(error => console.log(error))
    }
    

    A Form component

    Now we can use state.editingIdeaId in the render function, so that instead of displaying just a normal idea tile, we can display a form.

    Inside the map function, let’s change the return value to a conditional statement, which renders an IdeaForm component if an idea’s id matches state.editingIdeaId, otherwise rendering an Idea component:

    {this.state.ideas.map((idea) => {
      if(this.state.editingIdeaId === idea.id) {
        return(<IdeaForm idea={idea} key={idea.id} />)
      } else {
        return (<Idea idea={idea} key={idea.id} />)
      }
    })}
    

    Let’s import the IdeaForm component in IdeasContainer:

    import IdeaForm from './IdeaForm'
    

    And let’s define it in IdeaForm.js. We’ll start with a simple class component, which renders a form with two input fields for the idea title and body:

    import React, { Component } from 'react'
    import axios from 'axios'
    
    class IdeaForm extends Component {
      constructor(props) {
        super(props)
        this.state = {
        }
      }
    
      render() {
        return (
          <div className="tile">
            <form>
              <input className='input' type="text"
                name="title" placeholder='Enter a Title' />
              <textarea className='input' name="body"
                placeholder='Describe your idea'></textarea>
            </form>
          </div>
        );
      }
    }
    
    export default IdeaForm
    

    Let’s add a bit of CSS in App.css to style the form:

    .input {
      border: 0;
      background: none;
      outline: none;
      margin-top:10px;
      width: 140px;
      font-size: 11px;
    }
    
    .input:focus {
      border: solid 1px lightgrey;
    }
    
    textarea {
      resize: none;
      height: 90px;
      font-size: 11px;
    }
    

    Now when we click on the new idea button, a new tile appears with a form in it:

    Styled form for editing new idea

    Now let’s make this form functional!

    We need to hook up the form input fields to the state.

    First, let’s initialize the IdeaForm component state values from the idea prop that it receives from IdeasContainer:

    class IdeaForm extends Component {
      constructor(props) {
        super(props)
        this.state = {
          title: this.props.idea.title,
          body: this.props.idea.body
        }
      }
    

    Then set the form field values to their corresponding state values and set an onChange handler:

    <form>
      <input className='input' type="text"
        name="title" placeholder='Enter a Title'
        value={this.state.title} onChange={this.handleInput} />
      <textarea className='input' name="body"
        placeholder='Describe your idea'
        value={this.state.body} onChange={this.handleInput}>
      </textarea>
    </form>
    

    We’ll define handleInput such that, when we type in either of the input fields, the corresponding state value and then the value of the field gets updated:

    handleInput = (e) => {
      this.setState({[e.target.name]: e.target.value})
    }
    

    Tracking state changes in React Developer Tools

    Let’s see these state changes in action with the React Developer Tools browser extension. You can get it for Chrome here and for Firefox here.

    Once you have it installed, refresh the app page and open the developer console. You should see a new React tab.

    When you click on it, you’ll see our app components tree on the left and all the props and state associated with each component on the right.

    React developer tools showing state updates

    Now we’re updating the form fields, but we’re still not saving the edited idea. So the next thing needed is that, when we blur out of a form field, we want to submit the form and update the idea.

    API endpoint for updating ideas

    First, we need to define an API endpoint for updating ideas. So let’s add an update action in IdeasController:

    def update
      @idea = Idea.find(params[:id])
      @idea.update_attributes(idea_params)
      render json: @idea
    end
    

    Back in IdeaForm.js, we’ll set an onBlur handler called handleBlur to the form:

    <form onBlur={this.handleBlur} >
    

    We’ll define handleBlur to make a PUT call to our API endpoint for updating ideas with idea data from the state. For now, let’s just log the response to the console and see if our call works:

    handleBlur = () => {
      const idea = {
        title: this.state.title,
        body: this.state.body
      }
    
      axios.put(
        `http://localhost:3001/api/v1/ideas/${this.props.idea.id}`,
        {
          idea: idea
        })
      .then(response => {
        console.log(response)
      })
      .catch(error => console.log(error))
    }
    

    We also need to import axios in this file to be able to use it:

    import axios from 'axios'
    

    Now if we click on the new idea button, edit its title and blur out of that field, we’ll see our API response logged in the console, with the new edited idea data.

    The same thing happens if we edit the body and blur out of that field.

    Checking edited idea data in the console

    So our onBlur handler works and we can edit our new idea, but we also need to send the edited idea data back up to IdeasContainer so that it can update its own state too.

    Otherwise, state.ideas won’t have the updated value of the idea we just edited.

    We’ll use a method called updateIdea, which we’ll pass as a prop from IdeasContainer to IdeaForm. We’ll call updateIdea with the response data from our API call:

    handleBlur = () => {
      const idea = {
        title: this.state.title,
        body: this.state.body
      }
    
      axios.put(
        `http://localhost:3001/api/v1/ideas/${this.props.idea.id}`,
        {
          idea: idea
        })
      .then(response => {
        console.log(response)
        this.props.updateIdea(response.data)
      })
      .catch(error => console.log(error))
    }
    

    Now in IdeasContainer, let’s send an updateIdea function as a prop to IdeaForm:

    <IdeaForm idea={idea} key={idea.id}
     updateIdea={this.updateIdea} />
    

    Let’s define the function to do an immutable update of the idea in state.ideas:

    updateIdea = (idea) => {
      const ideaIndex = this.state.ideas.findIndex(x => x.id === idea.id)
      const ideas = update(this.state.ideas, {
        [ideaIndex]: { $set: idea }
      })
      this.setState({ideas: ideas})
    }
    

    First, we find the index of the edited idea in the array, and then use the $set command to replace the old value with the new one. Finally, we call setState to update state.ideas.

    We can see this in action in the browser with the React Developer Tools tab open.

    IdeasContainer state updates

    Displaying a success notification

    Now we can add a new idea and edit it, but the user gets no visual feedback or confirmation when the idea is saved. So let’s add a notification message to tell the user when an idea has been successfully saved.

    Let’s add a span next to the new idea button to display a notification from a value in state:

    <span className="notification">
      {this.state.notification}
    </span>
    

    Let’s initialize state.notification as an empty string:

    constructor(props) {
      super(props)
      this.state = {
        ideas: [],
        editingIdeaId: null,
        notification: ''
      }
    }
    

    Now every time an idea gets updated, we’ll update state.notification with a success notification we want to show to the user.

    So in the setState call in updateIdea, in addition to updating ideas, let’s also update notification:

    this.setState({
      ideas: ideas,
      notification: 'All changes saved'
    })
    

    Now when we edit an idea and blur out of the input field, the idea gets saved and we see the success notification.

    Notification message on successful updates to an idea

    We also want to reset the notification as soon as the user makes a change that hasn’t been saved yet.

    So in the handleInput function of the IdeaForm component, let’s call a function called resetNotification to reset the notification message:

    handleInput = (e) => {
      this.props.resetNotification()
      this.setState({[e.target.name]: e.target.value})
    }
    

    Now, inside the render function of IdeasContainer, let’s also pass resetNotification as a prop to IdeaForm:

    <IdeaForm idea={idea} key={idea.id}
      updateIdea={this.updateIdea}
      resetNotification={this.resetNotification} />
    

    Let’s define resetNotification as:

    resetNotification = () => {
      this.setState({notification: ''})
    }
    

    Now after a success notification appears, if we edit the idea again, the notification disappears.

    Reset notification message on unsaved edits

    Editing an existing idea

    Next, let’s add the ability to edit an existing idea. When we click on an idea tile, we want to change the tile so that it replaces the Idea component with an IdeaForm component to edit that idea.

    Then we can edit the idea and it will get saved on blur.

    In order to add this feature, we need to add a click handler on our idea tiles.

    So first we need to convert our Idea component from a functional component into a class component and then we can set define a click handler function handleClick for the title and body.

    import React, { Component } from 'react'
    
    class Idea extends Component {
    
      handleClick = () => {
        this.props.onClick(this.props.idea.id)
      }
    
      render () {
        return(
          <div className="tile">
            <h4 onClick={this.handleClick}>
              {this.props.idea.title}
            </h4>
            <p onClick={this.handleClick}>
              {this.props.idea.body}
            </p>
          </div>
        )
      }
    }
    
    export default Idea
    

    Note that we have to add this.props. to use the props value, because unlike in the functional component, we are no longer destructuring the props object.

    handleClick calls this.props.onClick with the idea id.

    Now, inside the render function of IdeasContainer, let’s also pass onClick as a prop to Idea:

    return (<Idea idea={idea} key={idea.id} onClick={this.enableEditing} />)
    

    We’ll define enableEditing to set the value of state.editingIdeaId to the clicked idea’s id:

    enableEditing = (id) => {
      this.setState({editingIdeaId: id})
    }
    

    Now when we click on a tile, it instantly becomes editable!

    Click on an idea tile to edit it

    When we click on a tile, once the form appears, let’s also set the cursor focus to the title input field.

    We can do that by adding a ref on the title input field in IdeaForm:

    <input className='input' type="text"
      name="title" placeholder='Enter a Title'
      value={this.state.title} onChange={this.handleInput}
      ref={this.props.titleRef} />
    

    We need to pass the ref as a prop, because we want to use it in the parent component IdeasContainer, where we can define the ref as a callback function:

    <IdeaForm idea={idea} key={idea.id}
      updateIdea={this.updateIdea}
      titleRef= {input => this.title = input}
      resetNotification={this.resetNotification} />
    

    Now we can use this ref in enableEditing to set the focus in the title input field:

    enableEditing = (id) => {
      this.setState({editingIdeaId: id},
        () => { this.title.focus() })
    }
    

    Notice that we didn’t call this.title.focus() as a separate function after calling setState. Instead, we passed it to setState inside a callback as a second argument.

    We did this because setState doesn’t always immediately update the component. By passing our focus call in a callback, we make sure that it gets called only after the component has been updated.

    Now if we try the app in a browser, when we click on an idea tile, it becomes editable with a form and the cursor gets focused inside its title input field.

    Click to edit idea and set focus to input field

    So now we can add and edit ideas.

    Deleting an idea

    Finally, we want to be able to delete ideas.

    When we hover over an idea tile, we want a delete button (in the form of a red cross) to appear in the top right corner. Clicking that cross should delete the idea and remove the tile from the board.

    So let’s start by adding some markup and CSS to display the delete button on hover.

    In the Idea component, add a span with a class deleteButton and the text ‘x’:

    <div className="tile">
      <span className="deleteButton">
        x
      </span>
    

    Then let’s add some CSS in App.css to hide this span by default and make it visible when we hover over a tile:

    .deleteButton {
      visibility: hidden;
      float: right;
      margin: 5px;
      font-size: 14px;
      cursor: pointer;
      color: red;
    }
    
    .tile:hover .deleteButton {
      visibility: visible;
    }
    

    Delete button appears on hovering over a tile

    Next, let’s add a click handler handleDelete to this delete button, which then deletes the idea:

    <span className="deleteButton" onClick={this.handleDelete}>
      x
    </span>
    

    Similar to handleClick, we’ll define handleDelete as an arrow function that calls another function this.props.onDelete with the tile’s idea id:

    handleDelete = () => {
      this.props.onDelete(this.props.idea.id)
    }
    

    Let’s pass onDelete as a prop from IdeasContainer:

    <Idea idea={idea} key={idea.id}
      onClick={this.enableEditing}
      onDelete={this.deleteIdea} />
    

    We’ll define deleteIdea in a moment, but first let’s add an API endpoint for deleting ideas in IdeasController:

    def destroy
      @idea = Idea.find(params[:id])
      if @idea.destroy
        head :no_content, status: :ok
      else
        render json: @idea.errors, status: :unprocessable_entity
      end
    end
    

    Now let’s define deleteIdea in IdeasContainer as a function that makes a DELETE call to our API with the idea id and, on success, updates state.ideas:

    deleteIdea = (id) => {
      axios.delete(`http://localhost:3001/api/v1/ideas/${id}`)
      .then(response => {
        const ideaIndex = this.state.ideas.findIndex(x => x.id === id)
        const ideas = update(this.state.ideas, { $splice: [[ideaIndex, 1]]})
        this.setState({ideas: ideas})
      })
      .catch(error => console.log(error))
    }
    

    Once again, we look up the index of the deleted idea, use update with the $splice command to create a new array of ideas, and then update state.ideas with that.

    Now we can try it in the browser. When we hover over an idea tile, the red delete button appears. Clicking on it deletes the idea and removes the tile from the board.

    Click the delete button to delete ideas

    Hurray, we now have a functional app with all the basic CRUD functionality!

    Wrap Up

    In this tutorial, we built a complete CRUD app using a Rails 5.1 API and a front-end React app.

    Our API has three endpoints, one each for creating, updating and deleting ideas.

    We used Create React App to make our React app. This made setup completely painless and easy. We could dive straight into building our app instead of configuring anything.

    We used axios for making Ajax calls to the API and immutability-helper to make data updates.

    In a future tutorial, we can look at how to deploy this app to a production server and also add some animations and transitions to spice up the UI. For example, we could fade in new idea tiles and fade out deleted tiles, fade in and out notification messages.

    You can watch a video version of this tutorial here.

    You can see the full code for the app on GitHub:

    Ideaboard Rails API

    Ideaboard React frontend