Learn Ruby on Rails Book

Showing notifications

Creating a Toastr component

Yes. You read it right. We at BigBinary 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 often send json responses with a particular key called notice. Again this is a convention we follow at BigBinary. The notice key will always contain the message that needs to be shown as a notification. Example:

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

So when this response is received at the front-end 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:

1mdkir -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

Great. Now that we have added the necessary packages, the next natural thing would be to actually refer their documentation on how to use the package :D. But since that would take some time, let's actually get to the point and do the next steps.

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 that adds a feature/visual utility to the UI. We need to add these css files into 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. Since we require these libraries in the javascript codebase we can add them to application.scss file.

So append these 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 will be as intended.

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 into 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.

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.

Append the following to app/javascript/src/apis/axios.js:

1const handleSuccessResponse = response => {
2  if (response) {
3    response.success = response.status === 200;
4    if (response.data.notice) {
5      Toastr.success(response.data.notice);
6    }
7  }
8  return response;
9};
10
11const handleErrorResponse = error => {
12  if (error.response?.status === 401) {
13    setToLocalStorage({ authToken: null, email: null, userId: null });
14  }
15  Toastr.error(
16    error.response?.data?.error ||
17      error.response?.data?.notice ||
18      error.message ||
19      error.notice ||
20      "Something went wrong!"
21  );
22  if (error.response?.status === 423) {
23    window.location.href = "/";
24  }
25  return Promise.reject(error);
26};
27
28export const registerIntercepts = () => {
29  axios.interceptors.response.use(handleSuccessResponse, error =>
30    handleErrorResponse(error)
31  );
32};

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.

Add the following in app/javascript/src/App.jsx:

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 message of the rails errors object as json response, then those notifications should popup. 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"