Overcome your JavaScript tooling fears and build a modern front end for your Rails app. Set up, build and deploy a Rails/React SPA with front end routing that still relies on good old ActiveRecord with absolutely no time wasted on configuration. (If you prefer to go to the repo right away, follow this link)

You do not have to to be afraid of JavaScript anymore

From inside of Ruby/Rails “omakase” bubble, modern JavaScript territory looks like the Wild West, and reasonably so. There are numerous blog posts about the subject. In 2016, fresh out of Rails bootcamp, I was staring at JS message boards, paralyzed with awe. The first attempt to write some proper code killed my appetite for months. Have you ever tried setting up ESLint for the Airbnb style guide? I have not written a single line of code yet but was already knee-deep in StackOverflow, and my project had hundreds of foreign files inside its node_modules folder. It made me sick. Crying on shoulders of fellow Rubyists who chose to stick with jQuery for most of their needs brought some consolation, but the feeling of being left behind the herd that does cool JS stuff never really left. Then The Good News arrived.

Finally, there appeared to be a Rails way of writing modern JS! Convention over configuration and such. Does it mean one can embrace both Rails and modern front end practices and be done with JavaScript anxiety? Let us find out.

A note about style: This tutorial uses ES6 JavaScript syntax and all its goodness like classes and arrow functions together with Standard style. Yes, I know, that means no semicolons and a space before parentheses in function declarations. That is a small price to pay for the simplicity of linting and reformatting Standard (it has nice plugins both for Sublime and Atom). Use SemiStandard if you absolutely cannot let go of those semicolons.

What we are building

Relax, it is not going to be another ToDo app. It will be an app to display inspirational quotes. The problem with 90% of React tutorials out there is that they pretend back end does not exist, and your data is just there, ready to be rendered in full React awesomeness. This time, let us not pretend props (I assume you already picked up on React lingo) come out of nowhere. Our quotes will be stored in the production-ready database and served to the front end as JSONs over /api-namespaced endpoints. The whole routing, from the user’s point of view (each quote is going to have its unique shareable URL), will be implemented in React Router, so, basically, we will have two completely different sets of routes in our app. Here is what it is going to look like in half an hour or so:

Setting it up

In order for everything to run smoothly, make sure your Rails is 5.1 or higher, you got Node installed on your machine with a version higher than 6.4, and you also have Yarn, which is a new fancy way to manage JavaScript dependencies, so you never have to run npm install --save again.

Do you remember when I said you would not have to deal with configuration? That is the main promise of the Webpacker gem that comes bundled with Rails since version 5.1. Rails Assets Pipeline will still be present in your project, but you do not have to rely on it anymore. Webpacker is there to make your Rails app play nicely with webpack, an asset builder that quickly becomes a de-facto standard in the JavaScript world.

Without further ado, here is a single command to scaffold a fresh Rails/React project:

rails new rails-react-starter-tutorial --webpack=react --database=postgresql -T 

(We also add a setting to configure our app for PostgreSQL, because we are going to deploy to Heroku and it makes sense to use the same DB for development and production. -T is to omit tests, as we will not need them for this tutorial)

Tap ‘Enter’ and enjoy the output. Among other things, the generator runs this command for you:

yarn add webpack webpack-merge js-yaml path-complete-extname webpack-manifest-plugin babel-loader@7.x coffee-loader coffee-script babel-core babel-preset-env babel-polyfill compression-webpack-plugin rails-erb-loader glob extract-text-webpack-plugin node-sass file-loader sass-loader css-loader style-loader postcss-loader autoprefixer postcss-smart-import precss resolve-url-loader babel-plugin-syntax-dynamic-import babel-plugin-transform-class-properties 

Overall, Yarn will add over 700 JavaScript dependencies in your node_modules folder (which is automatically added to your .gitignore, so you will not be checking thousands of library files into your repository). These are all the tools you need to start writing modern JavaScript right away. The generator will also add the webpack-dev-server development dependency, which is the real star of the show: it enables hot reloading and makes development a breeze — you do not even have to hit refresh in your browser to see changes to the UI, just save the code you are writing.

In your /config folder, you will find the default configuration for Webpack for both development and production environments. It also comes with all the loaders you are going to need to start. Good-bye, tooling confusion, so long!

Your Git repo is also initialized by the installation. Now, it is probably a good idea to git add . and git commit.

Hello, React

You all remember /assets/javascripts which is a part of Asset Pipeline. Well, you can forget about this folder for the whole length of this tutorial (except for some CSS at the end). Having initialized the project with the --webpack=react option, you got another one: /app/javascript  — this is exactly where we are going to write all our React code.

Note that the Rails generator has already put a folder named /packs inside of /app/javascript. It contains a file named  hello_react.jsx, so we can test our first React component right away.

Just before we start, we have to create a file named Procfile.dev in the root folder of our project. Put these two lines inside:

web: bundle exec rails s
webpacker: ./bin/webpack-dev-server

It is going to make our life easier by instructing a process manager utility like Foreman (go ahead and install it, if you do not have it yet) to run two processes simultaneously: rails s you all know and love and webpack-dev-server that will analyse changes in our app/javascript folder and rebuild the front end on the fly.

With Webpacker, React components are embedded into Rails views with a javascript_pack_tag helper. So, we need a view. Get back to the Ruby world for a minute.

In the console:

rails g controller pages home

Now, edit routes.rb to contain the only route:

root to: "pages#home"

Finally, replace the contents of the freshly generated home.html.erb with a single line of code:

<%= javascript_pack_tag 'hello_react' %>

Time to start our server!

foreman start -f Procfile.dev -p 3000

You will see that webpack is taking care of all the .js and .jsx files, bundling them into a single hello_react.js and serving it from http://0.0.0.0:8080/packs/. Head to http://localhost:3000 to see “Hello React!” appear in the browser.

If you want to see hot reloading in action, pass another name prop to <Hello /> component on line 23 of hello_react.jsx. Save the file and go back to the browser to see changes appear automatically. Magic!

You may be startled to see hello_react.js taking 2.9 MB of space. That is because the bundle includes the entirety of the node_modules folder in non-minified and non-uglified form. No worries, after compiling assets for production, the size will go down by at least a factor of 10.

Rails part

Well, that was easy! However, putting “Hello, World” on screen is hardly a complex programming concept (unless you are into esoteric languages like JSFuck). It is just a sign everything is working as intended and you are ready to actually build something useful. So, let us get to business. First, we need to set up our back end with Rails. We will follow the usual Model->Controller->View flow.

Our app is showing quotes, so we need a Quote model. Let us generate it. It will have just two attributes, both strings.

rails g model Quote text author

Then, let us add a seed file and put five inspirational quotes there (feel free to pick your own!)

# db/seeds.rb
Quote.delete_all

Quote.create! (
  [
    {
      text: "Be yourself; everyone else is already taken.",
      author: "Oscar Wilde"
    },
    {
      text: "Two things are infinite: the universe and human stupidity; " \
            "and I'm not sure about the universe.",
      author: "Albert Einstein"
    },
    {
      text: "So many books, so little time.",
      author: "Frank Zappa"
    },
    {
      text: "Be the change that you wish to see in the world",
      author: "Mahatma Gandhi"
    },
    {
      text: "If you tell the truth, you don't have to remember anything.",
      author: "Mark Twain"
    }
  ]
)
puts "Quotes seeded!"

Great! Now, execute rails db:create db:migrate db:seed, to create our DB and seed it (make sure you have PostgreSQL set up on your machine). Go to rails c and type Quote.all, to make sure it worked.

Now, let us generate a controller for our quotes in a separate API namespace, so we do not mix our routes. Remember, we only need Rails to serve JSONs and bring us to home.html.erb, where our main React component will mount; the quote-to-quote navigation will be handled inside React.

rails g controller api/quotes

Thanks to the generator, the resulting quotes_controller.rb already inherits from Api::QuotesController and lives in the /api folder inside our app/controllers/. Go to that file and add the only method we are going to need: a classic CRUD show.

# app/controllers/api/quotes_controller.rb
class Api::QuotesController < ApplicationController
  def show
    @quote = Quote.find(params[:id])
  end
end

Time to update our routes.rb.

# config/routes.rb
Rails.application.routes.draw do
  root to: "pages#home"
  namespace :api, defaults: { format: :json } do
    resources :quotes, only: [ :show ]
  end
end

Now, a GET request to /api/quotes/:id will return a JSON with data retrieved in quotes#show. We still need a view for that, and we are going to use jbuilder. It comes together with Rails, so no need to install a gem. Create a file named show.json.jbuilder inside of app/views/api/quotes/. Put this inside:

# app/views/api/quotes/show.json.jbuilder
json.extract! @quote, :id, :text, :author

Kill your foreman, if it is still running, and restart it (remember, we are doing it with foreman start -f Procfile.dev -p 3000 instead of rails c). In your browser, head to http://localhost:3000/api/quotes/1. You should see a JSON rendered to the page.

{"id":1,"text":"Be yourself; everyone else is already taken.","author":"Oscar Wilde"}

If you do, you are done with the Rails part! We will introduce some minor tweaks as we work on our React component, but this is largely it. Git add and commit happily.

Finding our way in React

Time to do away with our hello_react example. Webpacker recommends a certain way to go about the folder structure for your JS packs, and we are not going to fight it. Structuring your components and related files is one of the keys to maintainable code. It is so easy to get lost in JavaScript modules. Just adopt a strategy and stick to it. In the end, our /app/javascript folder is going to look like this:

app/javascript
├── packs
│   ├── application.js
│   └── quotes.js
└── quotes
    ├── components
    │   ├── App.jsx
    │   ├── QuoteFooter.jsx
    │   ├── QuoteNavigation.jsx
    │   ├── QuoteText.jsx
    │   └── QuotesDisplay.jsx
    └── index.js

First, create a /quotes folder inside /javascript, right next to the /packs folder that, for now, still contains hello_react.jsx. You can delete that file now or whenever you feel comfortable. Put a file named quotes.js inside /packs. It contains a single instruction and basically tells Webpacker to use /index.js in the corresponding folder. You will never touch that file again.

// app/javascript/packs/quotes.js
import 'quotes'

Personally, I find that notation misleading, as it seems to have nothing to do with ES6 import from statements, but looks confusingly similar. All the JS code you are going to write from now on will be inside of app/javascript/quotes. First, we need index.js

// app/javascript/quotes/index.js
import React from 'react'
import ReactDOM from 'react-dom'
import App from './components/App'


const quotes = document.querySelector('#quotes')
ReactDOM.render(<App />, quotes)

There we go! react and react-dom were installed by Yarn at project creation, and we are ready to use their modules in our code with ES6 import syntax. Then, we are selecting a parent <div> for our top React App component to mount, and we hook it up to the DOM.

Go and update your app/views/pages/home.html.erb, so it has a proper selector.

<div id="quotes"></div>
<%= javascript_pack_tag 'quotes' %>

Thinking in components

Time to write our first component! But first, create a /components folder inside /packs/quotes.

// app/javascript/quotes/components/App.jsx
import React from 'react'
import {
  BrowserRouter as Router,
  Route
} from 'react-router-dom'
import QuotesDisplay from './QuotesDisplay'

const App = (props) => (
  <Router>
	<div>
	  <Route
	    path='/'
	    component={QuotesDisplay}
	  />
	</div>
  </Router>
)

// You will need this on the bottom of each component file
// to make it available through ES6 'import' statements.

export default App

We are using React Router here, so we need to add it as a dependency. Go to console and run yarn add react-router-dom. Our App (sometimes called Root in other React Router examples) is a stateless or functional component: it contains a setup for our router, that, in its turn, will render components depending on the path user types into the browser. In our case, we will have a single component QuotesDisplay rendered at the root. This is not evident from the code above, but any component rendered by <Route> (<Router> and <Route> are what is called HOC, higher-order components, which is one of the advanced topics in React) will get three objects as props: location, match, and history. Feel free to play with them in React Dev Tools, once we are up and running. You should set them up in your browser of choice anyway!

We are interested in the contents of the location object, as it gives us the query string part of the path (called search in React Router terms). So, if the user goes to http://our-website/?quotes=1, we will be ready to parse the ?quotes=1 part and fetch a quote with ID 1 from the database. Sadly, recent releases of React Router do not parse query string for us, because the community around the project is still trying to figure out the correct implementation. Thus, we will need another dependency, just to parse query strings. While we are at it, let us also add axios to perform asynchronous AJAX requests from our React code, as we need to fetch our JSONs that Rails provides for us. So, in the console:

yarn add query-string axios

Now we are ready to implement our QuotesDisplay.jsx, which should also be located inside the /components folder.

// app/javascript/quotes/components/QuotesDisplay.jsx

import React from 'react';
import { Link } from 'react-router-dom';
import queryString from 'query-string';
import axios from 'axios';

class QuotesDisplay extends React.Component {
  constructor () {
    super();
    this.state = {
      quote: {}
    };
  }

  fetchQuote (id) {
    axios.get( `api/quotes/${id}` )
        .then(response => {
          this.setState({ quote: response.data });
        })
        .catch(error => {
          console.error(error);
        });
  }

  setQuoteIdFromQueryString (qs) {
    this.qsParams = queryString.parse(qs);
    if (this.qsParams.quote) {
      // assign quote ID from the URL's query string
      this.quoteId = Number(this.qsParams.quote);
    } else {
      this.quoteId = 1;
      // update URL in browser to reflect current quote in query string
      this.props.history.push(`/?quote=${this.quoteId}`);
    }
  }

  componentDidMount () {
    this.setQuoteIdFromQueryString(this.props.location.search);
    this.fetchQuote(this.quoteId);
  }

  componentWillReceiveProps (nextProps) {
    this.setQuoteIdFromQueryString(nextProps.location.search);
    this.fetchQuote(this.quoteId);
  }

  render () {
    const nextQuoteId = Number(this.state.quote.id) + 1;

    return (
      <div>
        <Link to={`/?quote=${nextQuoteId}`}>Next</Link>
        <p>{this.state.quote.text}</p>
        <p>{this.state.quote.author}</p>
      </div>
    );
  }
}

export default QuotesDisplay;

Our component has to deal with state, which is another fundamental React concept, so we cannot write it as a simple function anymore. We are declaring an ES6 class that “inherits” (I know that it is terribly wrong to use this word for JavaScript) from React.Component. We set an empty this.state in the constructor, which will hold our quote, once we get it as a JSON from our API endpoint. We declare the fetchQuote(id) method that will perform an AJAX call with axios to pull the corresponding quote from our database and update our state with it. We also have the setQuoteIdFromQueryString(qs) method that expects a query string (we will call it with location.search that we get from the router), parses quote ID from it, and sets it as an instance property with this.quoteId = .... A good explanation about different ways to hold React component data can be found here. If the query string is not found or it does not follow the ?quote=ID format, we will default the starting ID to 1 and update the URL field of our browser.

Now, take a look at componentDidMount() and componentWillReceiveProps(nextProps) — they are part of React  lifecycle methods. We need both of them, as we will mount our component, when we load the page, and then re-render it with different props, as we click on the “Next” link. Note that our link is another HOC from React Router, called <Link>. This is how we ensure each new quote will have a shareable unique URL. Inside each lifecycle method, we first parse a quote ID and then pass it to our AJAX call. Inside the render() method where React actually does its magic and intelligently updates our DOM, we guess the next quote ID by naively incrementing the current one by 1 and then passing it to the <Link> component.

Go to http://localhost:3000/ and see our proof-of-concept in action.  You click “Next”, the next quote is fetched, the URL is updated. That is pretty much it; what we have here is Rails and React working in concert. Time to congratulate ourselves, although not for long.

You can browse the code we have written up to this point in the tutorial here.

“Well, that’s not quite right...”

...you may say. We cannot go back to the previous quote, and we can only tap “Next” if we have (more) quotes in our database. Otherwise, our AJAX call will throw an error. If we put a non-existent or nonsensical ID into the URL, we will just render NaN to the page, which is not cool at all. We have also written at least 60 lines of JavaScript code, just to put three lines of text on the screen. “I can do it in jQuery with just 5 lines!”, you may think. And you will probably be right, but hold your horses. This is where React really starts to shine. Once you figured out your main component logic, tweaking it is a matter of changing few lines of code. Once the component is done, you can always reuse it later or break into smaller components to increase reusability even further and make your code DRYer. From my experience, overhead at the start is more than worth it, once you get further into the project.

In our first naive attempt, we assumed quote IDs in the database start at 1 and increment by 1, but, in the real world, it will rarely be so. For instance, if you add more quotes and reseed your file, seeds.rb will first delete all quotes from your database, but their IDs will not be reused, so it is hard to guess the starting ID. We need to find a way to know it for sure. Incrementing (or decrementing) by 1 is not a sane approach either: if you choose to delete a specific quote later, you will be left with a “hole” in your IDs. Let us make our Quote model smarter, by adding a couple of methods that will allow each quote to have virtual attributes for its predecessor and successor.

# app/models/quote.rb
class Quote < ApplicationRecord
  def next_id
    self.class.where('id > ?', self.id).pluck(:id).first
  end

  def previous_id
    self.class.where('id < ?', self.id).pluck(:id).last
  end
end

Let us also update our jbuilder view to represent this change in our JSON.

# app/views/api/quotes/show.json.jbuilder
json.extract! @quote, :id, :text, :author, :next_id, :previous_id

You should see the change by sending a GET to http://localhost:3000/api/quotes/1.

Now, you can update the render() method in your QuotesDisplay.jsx, to make use of these new attributes and introduce some conditional rendering.

// app/javascript/quotes/components/QuotesDisplay.jsx

render () {
  const quote = this.state.quote
  const nextQuoteId = quote.next_id
  const previousQuoteId = quote.previous_id
	
  return (
    <div>
      {previousQuoteId &&
	<Link to={`/?quote=${previousQuoteId}`}>
          Previous 
        </Link>
       }
       {nextQuoteId &&
         <Link to={`/?quote=${nextQuoteId}`}>
           Next
         </Link>
       }
	<p>{quote.text}</p>
	<p>{quote.author}</p>
      </div>
   )
} 

Okay, now we need to find out the starting ID. We can add another virtual attribute on the model to bundle a result of Quote.first.id with each new quote we get, but that seems rather redundant. After all, we only need to set the starting quote once. We will use a nice trick and pass the starting quote ID as an html attribute for the parent div of our React app. Then, we will use React’s one-way data flow to pass it down to our QuotesDisplay component. We will add one line of code to our pages_controller, which, for now, has no logic at all.

# app/controllers/pages_controller.rb
class PagesController < ApplicationController
  def home
    @first_quote_id = Quote.first.id
  end
end

Your home.html.erb view should now look like this:

# app/views/pages/home.html.erb
<div id='quotes' data-starting-quote-id='<%= @first_quote_id %>'></div>
<%= javascript_pack_tag 'quotes' %>

Modify the last line of index.js to pass the starting quote ID from parent element’s dataset as a prop to the <App /> component:

// app/javascript/quotes/index.js
// ... 	
ReactDOM.render(<App startingQuoteId={quotes.dataset.startingQuoteId} />, quotes)

Now, we need to modify our App component in App.jsx, to pass our startingQuoteId as a prop through Router and Route down to QuotesDisplay. With React Router, we need to fight the syntax a bit in order to pass props to the component rendered from Route (and it is not so obvious from the docs). This is how it is done:

// app/javascript/quotes/components/App.jsx
const App = (props) => (
  <Router startingQuoteId={props.startingQuoteId}>
	<div>
	  <Route
	    path='/'
	    startingQuoteId={props.startingQuoteId}
	    render={(routeProps) => <QuotesDisplay {...props} {...routeProps} />}
	  />
	</div>
  </Router>
)

Now we are able to use props.startingQuoteId inside our QuotesDisplay! On line 30 of QuotesDisplay.jsx, set the default quote ID to be this.quoteId = this.props.startingQuoteId (instead of 1).

To test, run rails db:seed in the console and refresh localhost:3000, to see our React component correctly picking up 6 as the starting ID.

Navigate our app here

Adding some style

Okay, time to make our page look decent. I find it easier to first build a static HTML/CSS layout in CodePen and then adopt it for the project. Check out the pen I created, to see, how our app is going to look like. For me, CSS is always the hardest part, so please do not judge the looks too harshly.

Angled brackets for navigation are part of FontAwesome, so we will need to add the font-awesome-rails gem to our Rails app. Kill your server, add gem 'font-awesome-rails' to your Gemfile, run bundle install from console, and restart the server. Go to app/assets/stylesheets/application.css, and change its extension to .scss. Put these two lines of code inside:

// app/assets/stylesheets/application.scss
@import "quotes";
@import "font-awesome";

In the same folder, create another file named quotes.scss, and put all the CSS from CodePen there. You can double check with the repo’s code. Now, we will introduce our new HTML structure into our components and break our QuotesDisplay into smaller bits as we go. Breaking down React components is a good practice and allows for more reusability — never hesitate to do it. Start with home.html.erb. Here are the new contents of the file:

<div class="flex-container">
  <div id="quotes" data-starting-quote-id="<%= @first_quote_id %>"></div>
</div>
<%= javascript_pack_tag 'quotes' %>

Here is our refactored QuotesDisplay.jsx:

import React from 'react';
import { Redirect } from 'react-router-dom';
import queryString from 'query-string';
import axios from 'axios';
import QuoteText from './QuoteText';
import QuoteNavigation from './QuoteNavigation';
import QuoteFooter from './QuoteFooter';

class QuotesDisplay extends React.Component {
  constructor () {
    super()
    this.state = {
      quote: {},
      fireRedirect: false
    }
  }

  fetchQuote (id) {
    axios.get(`api/quotes/${id}`)
        .then(response => {
          this.setState({ quote: response.data })
        })
        .catch(error => {
          console.error(error)
          this.setState({ fireRedirect: true })
        })
  }

  setQuoteIdFromQueryString (qs) {
    this.qsParams = queryString.parse(qs)
    if (this.qsParams.quote) {
      // assign quote ID from the URL's query string
      this.quoteId = Number(this.qsParams.quote)
    } else {
      this.quoteId = this.props.startingQuoteId
      // update URL in browser to reflect current quote in query string
      this.props.history.push(`/?quote=${this.quoteId}`)
    }
  }

  componentDidMount () {
    this.setQuoteIdFromQueryString(this.props.location.search)
    this.fetchQuote(this.quoteId)
  }

  componentWillReceiveProps (nextProps) {
    this.setQuoteIdFromQueryString(nextProps.location.search)
    this.fetchQuote(this.quoteId)
  }

  render () {
    const quote = this.state.quote
    const nextQuoteId = quote.next_id
    const previousQuoteId = quote.previous_id

    return (
      <div>
        <div className='quote-container'>
          {this.state.fireRedirect &&
          <Redirect to={'/'} />
            }
          {previousQuoteId &&
          <QuoteNavigation direction='previous' otherQuoteId={previousQuoteId} />
            }
          <QuoteText quote={this.state.quote} />
          {nextQuoteId &&
          <QuoteNavigation direction='next' otherQuoteId={nextQuoteId} />
            }
        </div>
        {this.state.quote.id !== parseInt(this.props.startingQuoteId, 10) &&
        <QuoteFooter startingQuoteId={this.props.startingQuoteId} />
          }
      </div>
    )
  }
}

export default QuotesDisplay

Note that we add three new sub-components: QuoteText to display the body of our quote, QuoteNavigation to display navigation (that will change to a left or right-pointed arrow, depending on direction prop), and QuoteFooter to display a footer allowing us to go back to the first quote. We also import Redirect from react-router-dom and conditionally render it (which will cause a redirect in our browser), if the new value fireRedirect in our state is set to true. We set it to false by default in the constructor and update it to true if our fetchQuote method errors out on the AJAX call. That means that, if someone tries to access our app with an URL ending with a query string /?quote=arghhhh, we will quietly redirect the user to the root and display the right starting quote.

Here are our three additional components; note that they are all stateless, so we do not need full ES6 classes for them — simple functions accepting props will do.

// app/javascript/quotes/components/QuoteText.jsx

import React from 'react';

const QuoteText = (props) => (
  <div className='quote'>
    <div className='quote-open'>“</div>
    <div className='quote-close'>”</div>
    <div className='quote-text'>
      {props.quote.text}
    </div>
    <div className='quote-author'>
      <em>— {props.quote.author}</em>
    </div>
  </div>
)

export default QuoteText
//app/javascript/quotes/components/QuoteFooter.jsx

import React from 'react';
import { Link } from 'react-router-dom';

const QuoteFooter = (props) => (
  <div id='footer'>
    <Link className='btn btn-primary' to={`/?quote=${props.startingQuoteId}`}>
      Back to Beginning
    </Link>
  </div>
)

export default QuoteFooter
// app/javascript/quotes/components/QuoteNavigation.jsx

import React from 'react';
import { Link } from 'react-router-dom';

const QuoteNavigation = (props) => {
  let element = null
  if (props.direction === 'previous') {
    element = (
      <Link className='link-previous' to={`/?quote=${props.otherQuoteId}`}>
        <i className='fa fa-angle-left' aria-hidden='true'><span /></i>
      </Link>
    )
  } else {
    element = (
      <Link className='link-next' to={`/?quote=${props.otherQuoteId}`}>
        <i className='fa fa-angle-right' aria-hidden='true'><span /></i>
      </Link>
    )
  }
  return element
}

export default QuoteNavigation

Verify that everything went well in the browser. Hooray! We now have a functional Rails/React app that looks a bit stylish. If you missed a step or two in the tutorial, you can find the final code for the project here.

Deploy

Okay, time to deploy our little app. With Heroku, it is an absolute no-brainer. It works with Rails and webpacker out of the box. Make sure you have a Heroku account and their set of CLI tools, and run these commands from your console:

heroku create YOUR_APP_NAME
git push heroku master 
heroku run rails db:migrate db:seed
heroku open 

And it’s a wrap! You can see the app in action here.

Thank you for following this tutorial, and feel free to ask any questions or suggest improvements in the comments. After all, learning while teaching is what I try to do here, and I started my road to React mastery not so long ago.