Migrating the Calendar appointments app to react_on_rails

Linked Projects:

Calreact
 
Lesson code - https://github.com/learnetto/calreact/tree/calreact-ror

In this lesson, we're going to migrate our React Calendar Appointments app from react-rails to using  the react_on_rails gem. We'll see what the differences are and it'll help you understand the concepts we outlined in the previous lesson in more detail.

Note that in this lesson, we're building on top of the app we built in lesson 4.4, when we were still using the react-rails gem (not 4.5 where we used the webpacker gem).

We're first going to remove the react-rails gem from the Gemfile and replace it with react_on_rails.

Note about react_on_rails gem version

At the time of recording this lesson, the latest react_on_rails gem version was 6.4.2, which did not depend on the yarn package manager, and only used npm.

However, the latest version of the gem requires yarn. So you can either specify the exact version in your Gemfile like this:

gem 'react_on_rails', '6.4.2'

Or if you want to use the latest gem with yarn, you can install yarn first and then replace all npm install commands in the lesson with yarn.


Next, let's remove the React and other utility JavaScript libraries (moment.js and react-datetime) from the application.js file. We're going to install moment.js and react-datetime from npm, so we can remove them from here. And we don't need components.js either.

Remove JS require statements from application.js


Another thing we need to remove is the addons config setting in application.rb
Remove the React addons setting in application.rb
Then let's install the react_on_rails gem and run through the setup process quickly.

Run bundle:
$ bundle

Then commit to git

$ git add -A
$ git commit -m "Remove react-rails gem. Add react_on_rails gem."

then run the generator

$ rails generate react_on_rails:install

And then bundle again and npm install:

$ bundle && npm install

Or bundle and yarn if you're using a new version of react_on_rails

$ bundle && yarn

Finally, run the app with foreman:

$ foreman start -f Procfile.dev

Now if we open it in a browser, the page loads but our components won't load because we haven't set them up with react_on_rails yet.
The page loads but we need to set up our components


The example hello world component works, so we know react_on_rails is working.
Example HelloWorld component inside our app


Ok, so now let's fix our components. They will live in the new client directory that react_on_rails created in the root folder of our app.

Let's rename the client/app/bundles/HelloWorld directory to client/app/bundles/Appointments.
Rename client/app/bundles/HelloWorld to client/app/bundles/Appointments
And then let's move our components from the app/assets/javascripts/components directory to this directory under client.
Move components to the client directory

Then we need to change the entry point for webpack in the client/app/bundles/Appointments/startup/registration.jsx file:

import ReactOnRails from 'react-on-rails';

import Appointments from '../components/appointments';

// This is how react_on_rails can see the Appointments in the browser.
ReactOnRails.register({
  Appointments,
});

Replace all instances of HelloWorld with Appointments.

Let's also delete the 3rd party library javascript files (moment and react-datetime) from the vendor assets directory.

Let's leave utils.js in app/assets for now because we'll use it later.
 
Ok now if we refresh the page, we get an error:
Uncaught Error: Cannot find module ".app/bundles/helloworld/startup/registration"

That's because we didn't change the entry file path in the Webpack config. So let's change that to from HelloWorld to Appointments:

  entry: [
    'es5-shim/es5-shim',
    'es5-shim/es5-sham',
    'babel-polyfill',
    './app/bundles/Appointments/startup/registration',
  ],

Now we need to restart webpack and refresh the page. We get a couple of new errors:

React is not defined!

This is because of one big change compared to react-rails. Because we had all our React component code in the app/assets directory, all components were automatically getting included in the asset pipeline.

But now that we have our components in the client directory (outside app/assets), we have to explicitly specify when we want to use code across files. That's the standard practice in using JavaScript modules using import and export statements.

The only thing that gets included in the asset pipeline by default is the final bundle that Webpack produces.

So let's fix this error by importing React in appointments.jsx:

import React from 'react';

We also need to export the Appointments component:

export default class Appointments extends React.Component {

Now if we refresh the page, the React and Appointments component errors are gone. But we get a different error - AppointmentForm is not defined.

AppointmentForm is not defined

So let's fix that in appointment_form.jsx.

import React from 'react';

export default class AppointmentForm extends React.Component {

And then we need to import it into the Appointments component:

import AppointmentForm from './appointment_form';

Then refresh and we get another error:
cannot find module './appointment_form'


We need to change the component file extensions:
Change component file extensions from .es6.jsx to .jsx
Let's remove .es6 because we haven't set webpack to recognise that extension. We're using es6 by default anyway so we don't need it. We can just use the .jsx extension.

Now let's reload and that error's fixed and we've got a different one:
AppointmentsList is not defined

So let's first export that in appointments_list.jsx:

import React from 'react';

export const AppointmentsList = ({appointments}) => 

Because it's a const, we just say export const, which makes it a named export. When we import it in appointments.jsx, we need to enclose it in curly braces, because it's a named import:

import { AppointmentsList } from './appointments_list';

If you want to avoid using named exports and imports, you can make it a default export by defining it below the component definition:
 
export default AppointmentsList

Ok now let's refresh and see what more fun awaits us!
Datetime is not defined!
I know this can be a bit annoying that we are getting so many errors and warnings.
But I'm purposely doing this step by step so you can see how react_on_rails and ES6 works.

Ok, let's fix the Datetime error by installing react-datetime from npm.

We just need to add it to our package.json file (the one in the client directory). Put it in the list of dependencies:

"react-datetime": "2.8.3",

And then run:

$ npm install

We get an unmet peer dependency error because react-datetime needs moment.js and we haven't installed it. We need moment anyway, so let's also add that to package.json and run npm install again.

Once that's done we can run foreman again.

 $ foreman start -f Procfile.dev

Remember we're not including anything by default. So we still need to import Datetime in our Appointments component:

import Datetime from 'react-datetime'

Now if we refresh, the Datetime error is fixed. But we get a different error:
Appointments: Cannot read property 'map' of undefined

That's coming from the AppointmentsList component, where we use map to loop through the  appointment records:

export const AppointmentsList = ({appointments}) => 
  <div>
    {appointments.map(function(appointment) {
      return (
        <Appointment appointment={appointment} key={appointment.id} />
      )
    })}
  </div>

It looks like the appointments prop is not being passed correctly.

Let's have a look in our index.html.erb file. Here we need to use the word props when passing the appointments data to the Appointments component. 

Unlike react-rails, react_on_rails requires we write it like this

= react_component('Appointments', props: {appointments: @appointments})

Ok, now let's try again. So that fixed the map error. Now we have a new error: 
Appointment is not defined

So let's fix that by first exporting it from appointment.jsx:

import React from 'react';

export const Appointment = ({appointment}) =>

And then import it in AppointmentsList:

import { Appointment } from './appointment'

Ok so Appointment is being loaded now. But moment is not defined because we haven't imported it yet.
moment is not defined

Now let's see where we're using moment. We use it in a utility function called formatDate in utils.js:

var formatDate = function(d) {
  return moment(d).format('MMMM DD YYYY, h:mm:ss a');
};

This file utils.js is still in our app/assets directory.  So first, let's move it to the client directory.

Under app/bundles/Appointments, let's make a new directory under client called utils and move the file there. Let's rename it to format.js.

So the file will now be at client/utils/format.js.

Now we need to export this function and let's import moment here.

import moment from 'moment';

export const formatDate = function(d) {
  return moment(d).format('MMMM DD YYYY, h:mm:ss a');
};

And then import formatDate in appointment.jsx as a named import:

import { formatDate } from '../utils/format';

Ok now, let's refresh our app.
Look ma, no errors!


We can see the form and our list of appointments.

Now let's try and make an appointment. Set a title and choose a date.

When I click on a date, the title gets reset. Looks like our state updates are not quite working correctly. If I enter the title again and submit the form, the appointment doesn't get added to the appointments list.

Let's have a look at the console. We have an error:
obj is not defined
That's coming from the handleChange function in AppointmentForm:

export default class AppointmentForm extends React.Component {
  handleChange (e) {
    const name = e.target.name;
    obj = {};
    obj[name] = e.target.value;
    this.props.onUserInput(obj);
  }

We need to make obj a const:

const obj = {};

Now if we refresh, we can set a title and choose a date without any errors. The title doesn't get reset. If we submit the form, we get an error:
Cannot read propery 'update' of undefined

That's coming from the addNewAppointment function in appointments.jsx because we don't have React addons. So let's add that.

Now although we used the update function from React addons in our previous lessons, this package has now been deprecated. The React team recommends using another package called immutability-helper. So let's use that.

This package has an update function which works just like the one we've been using.
So let's add it to the list of dependencies in our package.json file:

"immutability-helper": "2.1.1",

And run: 

$ npm install

And we need to restart our processes with foreman.

Then let's import update into the appointments.jsx file:

import update from 'immutability-helper';

And we can replace React.addons.update with just update in the addNewAppointment function:

const appointments = update(this.state.appointments, { $push: [appointment]});

The rest of the syntax remains exactly the same.

Now if we refresh, we can enter a title, choose a date and submit the form successfully. 
The appointment gets created and added to the appointments list.

It all works perfectly now!

So we've migrated our app from using the react-rails gem, to using the react_on_rails gem with Webpack.

We had to make some big changes to make this work.

We moved all of our React components code from the default Rails app/assets directory to a new client directory.

We also started using npm (or yarn) to install JavaScript dependencies.

And finally, we had to explicitly import and export all the components and utility libraries.

Liked this tutorial? Get more like this in your inbox