Skip to content

# Step 05 — Configuring React as Your Rails Front End

Gerald Goh edited this page Aug 11, 2020 · 1 revision

Configure Rails to use React on the frontend of the application, instead of its template engine. Take advantage of React rendering to create a more visually appealing homepage.

Rails, with the help of the Webpacker gem, bundles all JavaScript code into packs. These can be found in the packs directory at app/javascript/packs. Link these packs in Rails views using the javascript_pack_tag helper, and link stylesheets imported into the packs using the stylesheet_pack_tag helper. To create an entry point to React environment, add one of these packs to application layout.

First, rename the app/javascript/packs/hello_react.jsx file to app/javascript/packs/Index.jsx.

Add the following lines of code at the end of the head tag in the application layout file;

./app/views/layouts/application.html.erb

<!DOCTYPE html>
<html>
  <head>
    <title>Energy Tracker</title>
    <%= csrf_meta_tags %>
    <%= csp_meta_tag %>

    <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track': 'reload' %>
    <%= javascript_include_tag 'application', 'data-turbolinks-track': 'reload' %>
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <%= javascript_pack_tag 'Index' %>
  </head>

  <body>
    <%= yield %>
  </body>
</html>

Adding the JavaScript pack to application’s header makes all your JavaScript code available and executes the code in your Index.jsx file on the page whenever you run the app. Along with the JavaScript pack, you also added a meta viewport tag to control the dimensions and scaling of pages on your application.

Home Container

Since entry file is loaded onto the page, create a React component for homepage. Start by creating a components directory in the app/javascript directory:

$ mkdir ~/rails_react_recipe/app/javascript/components

The 'containers' (below) will house the component for the homepage, along with other React components in the application. The homepage will contain some text and a call to action button to Login or Signup.

Create a Home.jsx file in the components directory:

app/javascript/containers/Home.jsx

/* eslint-disable react/destructuring-assignment */
import React from 'react';
import Head from '../components/home/Head';
import Signin from '../components/home/Signin';
import Signup from '../components/home/Signup';

class Home extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      section: <Signin />,
    };
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick(e) {
    const sector = e.target.value;
    if (sector === 'Signup') {
      this.setState(() => ({
        section: <Signup />,
      }));
    } else {
      this.setState(() => ({
        section: <Signin />,
      }));
    }
  }

  render() {
    const sector = this.state.section;
    return (
      <div>
        <Head />
        <div className="container text-content sign-in-up">
          <div className="row justify-content-center">
            <div className="col-md-4 col-md-offset-4">
              <br />
              {/* Nav tabs */}
              <div className="text-center">
                <div className="btn-group">
                  <button
                    type="button"
                    value="Signup"
                    className="big btn btn-primary"
                    onClick={(e) => this.handleClick(e, 'value')}
                  >
                    <i className="fa fa-plus" />
                    {' '}
                    Create Account
                  </button>
                  <button
                    type="button"
                    value="Signin"
                    className="big btn btn-success"
                    onClick={(e) => this.handleClick(e, 'value')}
                  >
                    <i className="fa fa-user" />
                    {' '}
                    Login
                  </button>
                </div>
              </div>
              <p className="click2select">Tap to select</p>
              {sector}
            </div>
          </div>
        </div>
      </div>
    );
  }
}

export default Home;

Header Component

Create the following file;

app/javascript/components/Head.jsx

import React from 'react';

const Head = () => (
  <div>
    <nav className="navbar navbar-inverse bg-blue fixed-top">
      <h1 className="navbar-brand abs">Home</h1>
    </nav>
  </div>
);

export default Head;

Navbar component

Create the following file;

app/javascript/components/Navbar.jsx

/* eslint-disable no-return-assign, jsx-a11y/anchor-is-valid, import/no-named-as-default */
import React from 'react';
import SignoutBtn from './home/Signout';

const Navbar = () => (
  <footer id="sticky-footer" className="bg-grey fixed-bottom">
    <div className="container text-center">
      <ul className="row foot-row nav nav-pills tablist" role="tablist">
        <li className="col foot-col nav-item">
          <a onClick={() => window.location.href = '/addreading'} className="nav-link" data-toggle="pill" href="#">
            <i className="fas fa-chart-bar" />
            <p>Add Readings</p>
          </a>
        </li>
        <li className="col foot-col nav-item">
          <a onClick={() => window.location.href = '/readings'} className="nav-link" data-toggle="pill" href="#">
            <i className="fas fa-chart-line" />
            <p>Track.it</p>
          </a>
        </li>
        <li className="col foot-col nav-item">
          <a onClick={() => window.location.href = '/reading'} className="nav-link" data-toggle="pill" href="#">
            <i className="fas fa-chart-pie" />
            <p>Report</p>
          </a>
        </li>
        <li className="col foot-col nav-item">
          <a className="nav-link" data-toggle="pill" href="#">
            <SignoutBtn />
            {/* <i className="fas fa-ellipsis-h" />
            <p>More</p> */}
          </a>
        </li>
      </ul>
    </div>
  </footer>
);

export default Navbar;

Routes creation

In this code, React is imported and also the Link component from React Router. The Link component creates a hyperlink to navigate from one page to another. Then created and exported a functional component containing some Markup language for your homepage, styled with Bootstrap classes.

Create a routes directory in the app/javascript directory.

mkdir ~/energy-tracker/app/javascript/routes

The routes directory will contain a few routes with their corresponding components. Whenever any specified route is loaded, it will render its corresponding component to the browser.

In the routes directory, create an Index.jsx file:

$ nano ~/energy-tracker/app/javascript/routes/Index.jsx

Add the following code to it:

~/energy-tracker/app/javascript/routes/Index.jsx

import React from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
import { Provider } from 'react-redux';
import { compose, createStore } from 'redux';
import persistState from 'redux-localstorage';
import Home from '../containers/Home';
import Readings from '../containers/Readings';
import AddReading from '../containers/AddReading';
import Reading from '../containers/Reading';
import reducer from '../reducer/sessions';


const store = createStore(
  reducer, compose(persistState()),
);

export default (
  <Provider store={store}>
    <Router>
      <Switch>
        <Route exact path="/" component={Home} />
        <Route path="/addreading" component={AddReading} />
        <Route path="/readings" component={Readings} />
        <Route path="/reading" component={Reading} />
      </Switch>
    </Router>
  </Provider>
);

export { store };

In this Index.jsx route file, a couple of modules was import: the React module that allows us to use React, and the BrowserRouter, Route, and Switch modules from React Router, which together help us navigate from one route to another. Lastly,import Head and Signip component, which will be rendered whenever a request matches the root (/) route. To add more pages to your application, declare a route in this file and match it to the component you want to render for that page.

For React to be aware of the available routes and use them, the routes have to be available at the entry point to the application. To achieve this, you will render your routes in a component that React will render in your entry file.

app/javascript/packs/App.jsx

import React from 'react';
import Routes from '../routes/Index';

export default () => <>{Routes}</>;

In the App.jsx file, React is export and the route files that was created. Then exported a component that renders the routes within fragments. This component will be rendered at the entry point of the application, thereby making the routes available whenever the application is loaded.

Its time to render it in entry file. Open the entry Index.jsx file:

app/javascript/packs/Index.jsx

import React from 'react';
import { render } from 'react-dom';
import 'bootstrap/dist/css/bootstrap.min.css';
import 'bootstrap/dist/js/bootstrap.bundle.min';
import App from './App';

document.addEventListener('DOMContentLoaded', () => {
  render(
    <App />,
    document.body.appendChild(document.createElement('div')),
  );
});

In this code snippet, React is imported, the render method from ReactDOM, Bootstrap, jQuery, Popper.js, and App component. Using ReactDOM’s render method, render App component in a div element, which was appended to the body of the page. Whenever the application is loaded, React will render the content of the App component inside the div element on the page.

Add some CSS styles;

app/assets/stylesheets/custom.css.scss

@import 'bootstrap/dist/css/bootstrap';
@import "font-awesome";

$blue: #42B5E8;
$gray: #acafb4;
$green: #22C65B;
$grey: #313944;
$lighter-gray: #f3f4f6;

body {
  background-color: $lighter-gray !important;
  height: 100%;
}

.bg-blue {
  background-color: $blue;
}

.bg-grey {
  background-color: $grey;
}

nav {
  h1 {
    color: white;
    font-weight: 600;
  }
}

.navbar-brand.abs {
  text-align: center;
  width: 100%;
}

.text-content {
  margin-top: 70px;
}

#sticky-footer {
  flex-shrink: none;
  height: 50px;
}

.nav-pills 
.nav-link.active,
.nav-pills
.show > .nav-link {
	color: #fff;
	background-color: $blue !important;
}

.tablist {
  .col {
    padding-left: unset;
    padding-right: unset;
  }
}

.nav-pills .nav-link {
  border-radius: 0 !important;
  color: $gray;
  i {
    font-size: 1.5em;
    line-height: 1.3;
  }
  p {
    font-size: 13px;
    line-height: 17px;
  }
}

.nav-link {
  padding: 0 !important;
}

.sign-in-up .left-inner-addon {
  position: relative;
}

.sign-in-up .left-inner-addon input {
  padding-left: 30px;
}

.sign-in-up .left-inner-addon i {
  position: absolute;
  padding: 10px 12px;
  pointer-events: none;
}

.sign-in-up .right-inner-addon {
  position: relative;
}

.sign-in-up .right-inner-addon input {
  padding-right: 30px;
}

.sign-in-up .right-inner-addon i {
  position: absolute;
  right: 0px;
  padding: 17px 12px;
  pointer-events: none;
}

.sign-in-up .click2select {
  text-align: center;
  margin-top: 1em;
}

.sign-in-up .paywith {
  padding: 12px 12px 0 0;
  margin: 0;
  font-weight: bold;
}
.btn-danger, .btn-danger:hover {
  background-color: $green;
  border-color: $green;
}
.btn-primary, .btn-primary:hover {
  background-color: $blue;
  border-color: $blue;
}
  input[type="text"], input[type="password"], .btn {
  border-radius: 0px;
}

#regForm {
  background-color: #f3f4f6;
  height: 100vh;
  margin: auto;
  margin-bottom: 2em;
  padding: 0 40px 40px 40px;
  width: 70%;
  min-width: 300px;
}

input {
  padding: 10px;
  width: 100%;
  font-size: 17px;
  border: 1px solid #aaaaaa;
}

.btn-box {
  background-color: white;
  
  button {
    background-color: #a9dc95;    
    border: none;
    border-radius: 3px;
    color: #ffffff;    
    font-size: 17px;
    cursor: pointer;
    margin: 0.25em 0 0.25em 0;
    padding: 15px 65px;
  }
  
  button:hover {
    opacity: 0.8;
  }

  #nextBtn {
    margin-right: 0.25em;
  }
  
  #prevBtn {
    background-color: white;
    color: #bbbbbb;
    margin-left: 0.25em;
  }
}

.reading-header {
  background-color: white;
  height: 75px;
  text-align: center;
  h4 {
    margin: auto;
    padding: auto;
  }
}

.graph-content {
  margin: 10% 0 10% 0;
}

.reading-input {
  margin: 5% 0 5% 0;
}

.full-width-row {
  overflow-x: hidden;
}

.daily-stats {
  background-color: #f3f4f6;
  height: 100vh;
  margin: auto;
  margin-bottom: 2em;
  padding: 0 0px 40px 0px;
  width: 70%;
  min-width: 300px;
}

.circular-dimension {
  max-width: 40%;
}

.graph-box {
  background-color: white;
  justify-content: center;
  .row {
    .col {
      max-width: 100%;
    }
  }
}

.circular-box {  
  display: flex;
  justify-content: center;
  margin-top: 30px; 
}

.room-card {  
  margin: 10px 10px 10px 10px;
  .row{
    .col {
      background-color: white;
      padding: 10px 10px 10px 10px;
      text-align: center;
      i {
        color: $blue;
      }
    }
  }  
}

@media screen and (max-width: 720px) {
  #regForm,
  .daily-stats {
    width: 100%;
  }
  .circular-dimension {
    width: 100%;
  }
  .room-card {
    .row {
      .col {
        i {
          font-size: 3em;
        }
      }
    }
  }
}