Learn Ruby on Rails Book

Showing notifications

In this chapter we will add two new features. First feature is to display notification messages and second feature is to customize the HTTP requests using Axios interceptors.

Features

Our notification feature should work in the following manner:

  • Whenever an operation like creating a new task or deleting a task is performed, a notification message should be displayed which would let the user know of the result of the operation.

  • When the operation is successful, the notification should contain a success message and in case of a failure it should contain the error.

  • For success notifications, a checkbox icon should precede the message to denote a successful operation. Similarly in case of an error, there should be an alert icon. Small details like these can vastly improve the UI and UX of an application.

  • Notifications should always appear at the centre-bottom of our page as shown in the image attached below.

  • The notifications will be automatically shown upon getting a response from our server.

Showing notifications

Technical design

To implement these features, we need the following:

  • A notification component, we will call it Toastr for reasons mentioned in the next section. This Toastr component will display the notification message when a new notification arrives.

    Toastr component will be built using react-toastify library. Using react-toastify as a base will take care of a lot of low level implementation for us.

  • A component which will act as a container for the notification messages. We will name it ToastrComponent.

  • To display icons along with notification messages, we will install a third-party package called remixicon. This library has a lot of icons which we can use in our code. We are not required to import the icons in our components.

  • We can create a setAuthHeaders function to set default authorization headers for all HTTP requests. We will call this function in the App component.

  • Authorization data is usually stored in browser's local storage. We will add functions to get data from local storage as well as to set data in local storage.

    After declaring these functions we will call them to get authorization data from local storage and use the authorization data to set default authorization headers for all HTTP requests made using Axios.

  • Rather than invoking the notification component by ourselves, we can delegate the job of raising a notification automatically based on the status of the response using the Axios Interceptors.

  • API requests should contain authentication data by default. We shouldn't have to add authorization data every time we make a request.

Creating a Toastr component

Yes. You read it right. We named it Toastr, so that it doesn't collide with other library components which has names like Toaster, Toasts etc.

So a Toastr component's purpose is to display notifications like say "Task completed", "Preparing download..." etc with an intent like success, error etc.

Don't confuse this with the spinner logic. It's not the same. You can think of this as a notification that you receive in your phone or alert messages in your browser. Let's see how this can be useful for us.

In a Rails API based application, we send JSON responses with key called notice. This is a convention we will be following while building this application. The notice key will always contain the message that needs to be shown as a notification. Let's see an example.

1render json: { notice: 'Successfully deleted the item' }, status: :ok

So when this response is received at the frontend side, we need to shown a notification with an intent of success (since http status is ok or 200). That's pretty much the use case of a Toastr. Now let's actually create the component.

So the questions that you need ask yourself are:

  • Is this a React component? Yes. So we should name it with .jsx extension since it will contain jsx, obviously. This also helps with enhanced intellisense(we will get to it later).

  • Where should this file be created in our directory structure? Since this file is common for all other components, let's actually create a directory called Common and add this file inside it.

  • The directory name as well as component file name, should be in PascalCase. The same thing applies to the component naming.

Run the following commands to create our file:

1mkdir -p app/javascript/src/components/Common
2touch app/javascript/src/components/Common/Toastr.jsx

Now, in order to show the notifications, we need a base library which we can use and modify according to our requirements(remember the intents?). Thus, let's make use of the react-toastify package.

Also we will be showing some icons within the notifications. We at BigBinary mostly use only remixicons, since they are open source and contain most of the icon sets that we require. So let's add both these packages:

1yarn add react-toastify remixicon

We need the css files associated with each of these libraries in the Rails assets pipeline, in order to make it work.

This is an important step, and you should remember this whenever you are adding a new JavaScript package. We need to add the css files in the Rails assets pipeline.

It's a very huge topic and thus you can make use of the reference links at the bottom of this chapter, to learn more.

Let's append the following lines to app/javascript/stylesheets/application.scss:

1@import "react-toastify/dist/ReactToastify.min.css";
2@import url("https://cdn.jsdelivr.net/npm/remixicon@2.5.0/fonts/remixicon.css");

Awesome! Now we can use those packages and their styles, colors, transitions etc.

Sometimes these packages don't work as intended in heroku deployment. Yes. It's confusing. I know! But sometimes the packages work as intended locally, but not in heroku.

Then the first thing you should check is whether you have correctly added the CSS files in the assets pipeline.

Now let's complete our Toastr component. Add the following code snippet to app/javascript/src/components/Common/Toastr.jsx:

1import React from "react";
2import { toast, Slide } from "react-toastify";
3
4const ToastrComponent = ({ type, message }) => {
5  let icon;
6  switch (type) {
7  case "success":
8    icon = "ri-checkbox-circle-fill";
9    break;
10  case "error":
11    icon = "ri-alert-fill";
12    break;
13  case "info":
14    icon = "ri-information-fill";
15    break;
16  default:
17    icon = "ri-information-fill";
18    break;
19  }
20
21  return (
22    <div className="flex flex-row items-start justify-start">
23      <i className={icon}></i>
24      <p className="mx-4 font-medium leading-5 text-white">{message}</p>
25    </div>
26  );
27};
28
29const showToastr = message => {
30  toast.success(<ToastrComponent type="success" message={message} />, {
31    position: toast.POSITION.BOTTOM_CENTER,
32    transition: Slide,
33  });
34};
35
36const isError = e => e && e.stack && e.message;
37
38const showErrorToastr = error => {
39  const errorMessage = isError(error) ? error.message : error;
40  toast.error(<ToastrComponent type="error" message={errorMessage} />, {
41    position: toast.POSITION.BOTTOM_CENTER,
42    transition: Slide,
43  });
44};
45
46export const Toastr = {
47  success: showToastr,
48  error: showErrorToastr,
49};
50
51export default Toastr;

Done. Now we have a Toastr component which we can actually use in our code. But the question is how do we effectively use it?

Ideally, whenever we get a JSON response from our server, and if it contains the notice key or Rails errors object, then we need to show the notification.

But handling this step manually at each location where this response will be received, is a tedious task.

So let's make use of axios and the magical interceptors which it provides, to do this job for us. Let's see how it's done in the next section.

Before moving on to Axios interceptors, we should take a look at storing data like authorization token received through JSON responses in local storage.

Storing data in local storage

Let's create a helper to store required data in local storage and another helper to fetch the stored data.

First we can create a directory for helper files and a file called storage.js inside it:

1mkdir -p app/javascript/src/helpers/
2touch app/javascript/src/helpers/storage.js

Add the following to app/javascript/src/helpers/storage.js:

1const setToLocalStorage = ({ authToken, email, userId }) => {
2  localStorage.setItem("authToken", authToken);
3  localStorage.setItem("authEmail", email);
4  localStorage.setItem("authUserId", userId);
5};
6
7const getFromLocalStorage = key => {
8  return localStorage.getItem(key);
9};
10
11export { setToLocalStorage, getFromLocalStorage };

Now let's add a webpack alias for these helpers. Append the following into the alias key in config/webpack/alias.js:

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

Using Axios interceptors

Axios interceptors are the middleware that we use between the client and the server, so that it intercepts all the requests, and we can apply custom functionality.

These are technically, functions that Axios calls for every request.

You can use interceptors to transform the request before Axios sends it, or transform the response before Axios returns the response to your code from where it was invoked.

We have already used a similar Axios functionality for setting the headers with each request and wrapped it a custom function called setAuthHeaders.

Similarly let's actually write the interceptors in our project specific axios.js file.

Open app/javascript/src/apis/axios.js and add the following lines of code.

1import axios from "axios";
2import Toastr from "components/Common/Toastr";
3import { setToLocalStorage, getFromLocalStorage } from "helpers/storage.js";
4
5axios.defaults.baseURL = "/";
6
7export const setAuthHeaders = (setLoading = () => null) => {
8  axios.defaults.headers = {
9    Accept: "application/json",
10    "Content-Type": "application/json",
11    "X-CSRF-TOKEN": document
12      .querySelector('[name="csrf-token"]')
13      .getAttribute("content"),
14  };
15  const token = getFromLocalStorage("authToken");
16  const email = getFromLocalStorage("authEmail");
17  if (token && email) {
18    axios.defaults.headers["X-Auth-Email"] = email;
19    axios.defaults.headers["X-Auth-Token"] = token;
20  }
21  setLoading(false);
22};
23
24const handleSuccessResponse = (response) => {
25  if (response) {
26    response.success = response.status === 200;
27    if (response.data.notice) {
28      Toastr.success(response.data.notice);
29    }
30  }
31  return response;
32};
33
34const handleErrorResponse = (error) => {
35  if (error.response?.status === 401) {
36    setToLocalStorage({ authToken: null, email: null, userId: null });
37  }
38  Toastr.error(
39    error.response?.data?.error ||
40      error.response?.data?.notice ||
41      error.message ||
42      error.notice ||
43      "Something went wrong!"
44  );
45  if (error.response?.status === 423) {
46    window.location.href = "/";
47  }
48  return Promise.reject(error);
49};
50
51export const registerIntercepts = () => {
52  axios.interceptors.response.use(handleSuccessResponse, (error) =>
53    handleErrorResponse(error)
54  );
55};

Phew! That was a lot of code, but there is one more step that we have to do.

We need to register these interceptors, as well as add a ToastContainer for the react-toastify popups to show up in.

You can replace app/javascript/src/App.jsx with the following content:

1import React, { useEffect, useState } from "react";
2import { Route, Switch, BrowserRouter as Router } from "react-router-dom";
3import { ToastContainer } from "react-toastify";
4
5import Login from "components/Authentication/Login";
6import SignUp from "components/Authentication/SignUp";
7import CreateTask from "components/Tasks/CreateTask";
8import EditTask from "components/Tasks/EditTask";
9import Dashboard from "components/Dashboard";
10import PageLoader from "components/PageLoader";
11import { registerIntercepts, setAuthHeaders } from "apis/axios";
12import { initializeLogger } from "common/logger";
13
14const App = () => {
15  const [loading, setLoading] = useState(true);
16
17  useEffect(() => {
18    initializeLogger();
19    registerIntercepts();
20    setAuthHeaders(setLoading);
21  }, []);
22
23  if (loading) {
24    return (
25      <div className="h-screen">
26        <PageLoader />
27      </div>
28    );
29  }
30
31  return (
32    <Router>
33      <ToastContainer />
34      <Switch>
35        <Route exact path="/tasks/:slug/edit" component={EditTask} />
36        <Route exact path="/tasks/create" component={CreateTask} />
37        <Route exact path="/dashboard" component={Dashboard} />
38        <Route exact path="/sign-up" component={SignUp} />
39        <Route exact path="/" component={Login} />
40      </Switch>
41    </Router>
42  );
43};
44
45export default App;

Now we are officially done.

So whenever we send a JSON response with a notice key or when we send full messages from rails errors object in the error key of JSON response, then those notifications should show up.

The errors are handled in the handleErrorResponse function in axios.js.

These errors will be shown with an intent of error, which would make it popup in red background.

Likewise, we can create even further intents like say Toastr.info, Toastr.warning etc.

So just test it out, by say creating a new task.

You should be receiving a green colored notification if the task was successfully created.

1git add -A
2git commit -m "Created Toastr component and added Axios interceptors"
⌘K
    to navigateEnterto select Escto close
    Previous
    Next