Learn Ruby on Rails Book

Setting up React environment

React Router

React router is used for declarative routing in React. React Router uses a model called "dynamic routing". It means that routing takes place as your app is rendering, and not via a configuration or convention outside of the running app.

To install React Router, run the following command:

1yarn add react-router-dom

Then, run the following command:

1mkdir -p ./app/javascript/src/
2touch ./app/javascript/src/App.jsx

This creates App.jsx file.

The App.jsx component will act as the entry point for all our rendering.

Let's also add loading state into this component so that in upcoming sections we can set the axios headers synchronously.

Paste the follow contents into App.jsx:

1import React, { useEffect, useState } from "react";
2import { Route, Switch, BrowserRouter as Router } from "react-router-dom";
3
4const App = () => {
5  const [loading, setLoading] = useState(true);
6
7  return (
8    <Router>
9      <Switch>
10        <Route exact path="/" render={() => <div>Home</div>} />
11        <Route exact path="/about" render={() => <div>About</div>} />
12      </Switch>
13    </Router>
14  );
15};
16
17export default App;

Once the components are created and routing is implemented in the following chapters, at http://localhost:3000/, we should be able to see the Home component being rendered and at http://localhost:3000/about, we should be able to see the About component being rendered. This forms the basis for react-router.

Axios

Axios is a promise based HTTP client for the browser and Node.js. To install axios , run the following command:

1yarn add axios

HTTP requests can be sent easily by using different methods in axios .

Example: axios.get('https://httpbin.org/get') will send a GET request to the URL specified.

The axios requests are mostly async in nature. Thus at BigBinary we always embed all async blocks within the try-catch blocks to ensure that the code catches any exceptions that might occur as part of the call.

This is also a good practice in general which can moreover ensure an easier debugging experience for the developer.

Axios headers and defaults

Interceptors are methods which are triggered before the main method. A request interceptor is called before the actual call to the endpoint is made and a response interceptor is called before the promise is completed and the data is received from the callback:

1mkdir -p ./app/javascript/src/apis
2touch ./app/javascript/src/apis/axios.js

This creates a file axios.js inside apis directory.

As starters, we will use this file to set out Axios headers.

Rails by default adds CSRF counter measures for all requests.

Thus even for AJAX calls, we will have to provide a CSRF token, so that Rails will understand that the request is trust worthy.

In the upcoming chapters, when we reach authentication part, we need a scalable mechanism by which we can send the authentication token to our backend server with each request.

For all these cases, setting the Axios headers is the solution.

Paste the following to axios.js:

1import axios from "axios";
2
3axios.defaults.baseURL = "/";
4
5export const setAuthHeaders = (setLoading = () => null) => {
6  axios.defaults.headers = {
7    Accept: "application/json",
8    "Content-Type": "application/json",
9    "X-CSRF-TOKEN": document
10      .querySelector('[name="csrf-token"]')
11      .getAttribute("content"),
12  };
13  const token = localStorage.getItem("authToken");
14  const email = localStorage.getItem("authEmail");
15  if (token && email) {
16    axios.defaults.headers["X-Auth-Email"] = email;
17    axios.defaults.headers["X-Auth-Token"] = token;
18  }
19  setLoading(false);
20};

setAuthHeaders is a function invoked from App.jsx.

The CSRF token will be set in the X-CSRF-TOKEN header.

It then sets the page to loading and the function sets default headers from X-Auth-Email and X-Auth-Token.

The email and token will come into action once we reach the chapter dealing with authentication.

These tokens will then be sent with every request to the backend server.

The setLoading function is used to make sure that our App.jsx component is only rendered once these headers are set.

We usually show a page loader while waiting for this process to be completed.

Once the headers have been set, setLoading(false) removes the page loader and renders the desired component.

Now we need to invoke this setAuthHeaders method from our entry point, which is App.jsx.

Add the following code to App.jsx:

1import React, { useEffect, useState } from "react";
2import { Route, Switch, BrowserRouter as Router } from "react-router-dom";
3import { setAuthHeaders } from "apis/axios";
4// previous code if any
5
6const App = () => {
7  const [loading, setLoading] = useState(true);
8  // previous code if any
9
10  useEffect(() => {
11    setAuthHeaders(setLoading);
12  }, []);
13
14  // previous code without changes
15};

JS Logger

Our ESlint config does not allow us to use console.log inside of our app. You can read more about why we should avoid console.log in general over here.

This is how we have added a rule inside of our ESlint config that marks all console statements as errors:

1"no-console": "error"

However, during API calls to server or while communicating with another external service, errors are bound to occur. In such cases, we might want to log the errors that are caught from our try-catch block.

One can use js-logger in such cases. js-logger is a lightweight JavaScript Logger that has zero dependencies.

To install js-logger , run the following command:

1yarn add js-logger babel-plugin-js-logger

To initialize js-logger , run the following command from root of project:

1mkdir -p ./app/javascript/src/common
2touch ./app/javascript/src/common/logger.js

Inside logger.js , paste the following:

1export const initializeLogger = () => {
2  /* eslint no-undef: "off"*/
3  require("js-logger").useDefaults();
4};

Add "js-logger", to plugins key in babel.config.js so as to make logger function work:

1  plugins: [
2      "js-logger",
3      'babel-plugin-macros',
4      '@babel/plugin-syntax-dynamic-import',
5      isTestEnv && 'babel-plugin-dynamic-import-node',
6      '@babel/plugin-transform-destructuring',
7      ...
8  ]

Then, invoke initializeLogger() from App.jsx as follows:

1// previous imports
2import { initializeLogger } from "common/logger";
3
4const App = () => {
5  const [loading, setLoading] = useState(true);
6
7  useEffect(() => {
8    /*eslint no-undef: "off"*/
9    initializeLogger();
10    setAuthHeaders(setLoading);
11    // logger.info("Never use console.log");
12    // logger.error("Never use console.error");
13  }, []);
14
15  return (
16    <Router>
17      <Switch>
18        <Route exact path="/" render={() => <div>Home</div>} />
19        <Route exact path="/about" render={() => <div>About</div>} />
20      </Switch>
21    </Router>
22  );
23};
24
25export default App;

On running your server, you would see that js-logger would successfully log your messages in console, when you uncomment the logger statements.

Currently there is no use case of logging from App.jsx. Thus we suggest to consider the above mentioned logger statements as examples only.

CSRF token authenticity verification error

If you ever encounter the ActionController::InvalidAuthenticityToken error, then it means that your Axios requests are not sending the CSRF token while making requests to Rails server.

Thus the first point to check would be axios.js file and check whether code to set default headers have been added or not. Compare your axios.js file with the code given in the previous section.

Once that is verified, the next step is to check App.jsx, and verify that setAuthHeaders(setLoading) has been invoked from useEffect.

Please refer the previous sections in this chapter itself to understand how to set Axios headers.

Directory structure

Let's move our previously created apis , commons and components folders to a new folder called src within the javascript folder.

This is done so as to handle all the javascript/client code base much more elegantly and have more control over it.

Once done, we need to change the contexts. To do so, open the file app/javascript/packs/application.js and replace the following line:

1require("@rails/ujs").start();
2require("@rails/activestorage").start();
3require("channels");
4import "../stylesheets/application.scss";
5var componentRequireContext = require.context("src", true);
6var ReactRailsUJS = require("react_ujs");
7ReactRailsUJS.useContext(componentRequireContext);

The require.context inserted into packs/application.js is used to load components.

If you want to load components from a different directory, override it by calling ReactRailsUJS.useContext. If require fails to find your component, ReactRailsUJS falls back to the global namespace.

Aliases

Aliases allows us to create aliases to import or require certain modules more easily. Instead of doing:

1import authAPI from "../../apis/auth";

We can define an alias for the apis folder so that it can be accessed from any component without having to do relative import.

To do so, run the following command:

1touch ./config/webpack/alias.js

Paste the following into alias.js:

1module.exports = {
2  resolve: {
3    alias: {
4      apis: "src/apis",
5      common: "src/common",
6      components: "src/components",
7    },
8  },
9};

Then, add the following lines to config/webpack/environment.js to include the custom configuration we created to resolve aliases:

1const { environment } = require("@rails/webpacker");
2
3const aliasConfig = require("./alias");
4environment.config.merge(aliasConfig);
5
6module.exports = environment;

Adding this to environment.js makes it available in all environments like development, production etc.

Now, you can import from apis folder without having to resolve the entire path each time.

Example:

1import authAPI from "apis/auth";

Now let's commit these changes:

1git add -A
2git commit -m "Set up react environment"

References:

⌘K
    to navigateEnterto select Escto close
    Previous
    Next