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)
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.
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:
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
.
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.
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.
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' %>
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.
...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
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.
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.