DEV Community

Cover image for Enhancing User Experience: Implementing Notifications, Modals and Loaders in a React-based CMS
WebCraft Notes
WebCraft Notes

Posted on • Originally published at webcraft-notes.com

Enhancing User Experience: Implementing Notifications, Modals and Loaders in a React-based CMS

If you need a source code for this tutorial you can get it here.

Enhancing user experience is crucial for any modern Content Management System. In this article, we'll dive into implementing three essential UI components that will significantly improve the interactivity and feedback of our React-based CMS: Notifications for user feedback, and Loaders for managing loading states. These components will not only make our CMS more professional but also provide users with clear visual feedback during their interactions. So, let's start:

1. Implementing the Notification System

One of the most important things that we need to understand is that all these UX parts will be components that we will need to use on many different pages, so they need to be available globally. This task will solve our "Redux" storage, which we implemented in our previous article. Also, we will create a notification system where we can show many messages at the same time, that is why we will need an Array as a container for messages list and message component itself. Don't worry, everything will be clear when we start coding.

  • create a new folder in the "store", we will call it "base";

  • inside "base" folder we will create "base.reducer.js", "base.action.js", "base.selector.js", "base.types.js" files;

  • inside the "types" file we need to add two types for creating and removing messages;

const BASE_ACTION_TYPES = {
    PUSH_NEW_NOTIFICATION: 'base/PUSH_NEW_NOTIFICATION',
    REMOVE_NOTIFICATION: 'base/REMOVE_NOTIFICATION',
};

export default BASE_ACTION_TYPES;
Enter fullscreen mode Exit fullscreen mode
  • "state" will contain an empty array of our notifications, and "reducer" will have two cases (push new notification, and filter only active notifications);
import BASE_ACTION_TYPES from './base.types';

export const BASE_INITIAL_STATE = {
    notifications: [],
};
export const baseReducer = (state = BASE_INITIAL_STATE, action = {}) => {
    const { type, payload } = action;
    switch (type) {
        case BASE_ACTION_TYPES.PUSH_NEW_NOTIFICATION:
            return { ...state, notifications: [payload, ...state.notifications] };
        case BASE_ACTION_TYPES.REMOVE_NOTIFICATION:
            return { ...state, notifications: state.notifications.filter((n) => n.id !== payload) };
        default:
            return state;
    }
};
Enter fullscreen mode Exit fullscreen mode
  • the "action" file will also contain two actions, the first one will create a new notification with a unique ID and set a timeout value that will remove this notification from the list, and the second one will remove one notification by ID;
import { v4 as uuid } from 'uuid';
import BASE_ACTION_TYPES from './base.types';
import { createAction } from '../../utils/reducer.utils';

export const aPushNewNotification = (notification) => {
  return (dispatch) => {
    notification.id = uuid();
    dispatch(createAction(BASE_ACTION_TYPES.PUSH_NEW_NOTIFICATION, notification));
    setTimeout(() => {
      dispatch(aRemoveNotification(notification.id));
    }, 5000);
  };
};

export const aRemoveNotification = (id) =>
  createAction(BASE_ACTION_TYPES.REMOVE_NOTIFICATION, id);
Enter fullscreen mode Exit fullscreen mode
  • "selector" will simply return the list of notifications;
export const sNotificationsList = (state) => state.base.notifications;
Enter fullscreen mode Exit fullscreen mode

It looks great, we prepared our storage for implementing Notifications functionality into our React CMS, now we can move to the "notifications" components.

  • create a new "UI" folder inside "components", and a new "notification" folder;

  • add a new "Notification.component.jsx" file that will import the "useDispatch" hook, a list of icons from the "MUI" library, and a "remove" notification action. It will have a remove notification function that will call our action, and this component will return three types of notifications (success, error, warning), that will depend on the notification type;

import React from 'react';
import { useDispatch } from 'react-redux';
import { aRemoveNotification } from '../../../store/base/base.action';
import CheckIcon from '@mui/icons-material/Check';
import CloseOutlinedIcon from '@mui/icons-material/CloseOutlined';
import PriorityHighOutlinedIcon from '@mui/icons-material/PriorityHighOutlined';
import ReportProblemOutlinedIcon from '@mui/icons-material/ReportProblemOutlined';

const Notification = ({ notification }) => {
    const dispatch = useDispatch();
    const closeNotification = () => {
        dispatch(aRemoveNotification(notification.id));
    }

    if (notification.type === 'error') {
        return (
            <div className="notification">
                <div className="alert alert-error">
                    <div className="icon__wrapper">
                        <PriorityHighOutlinedIcon className="icon-style" />
                    </div>
                    <p>{notification.text}</p>
                    <CloseOutlinedIcon className="close" onClick={closeNotification} />
                </div>
            </div>
        );
    } else if (notification.type === 'success') {
        return (
            <div className="notification">
                <div className="alert alert-success">
                    <div className="icon__wrapper">
                        <CheckIcon className="icon-style" />
                    </div>
                    <p>{notification.text}</p>
                    <CloseOutlinedIcon className="close" onClick={closeNotification} />
                </div>
            </div>
        );
    } else if (notification.type === 'warning') {
        return (
            <div className="notification">
                <div className="alert alert-warning">
                    <div className="icon__wrapper">
                        <ReportProblemOutlinedIcon className="icon-style" />
                    </div>
                    <p>{notification.text}</p>
                    <CloseOutlinedIcon className="close" onClick={closeNotification} />
                </div>
            </div>
        );
    }
};
export default Notification;
Enter fullscreen mode Exit fullscreen mode
  • add a "NotificationContainer.component.jsx" file that will import our "Notification" component, and render a list of notifications from our storage;
import React from 'react';
import { useSelector } from 'react-redux';
import { sNotificationsList } from '../../../store/base/base.selector';
import Notification from './Notification.component';
import './notification.styles.scss';

const NotificationContainer = () => {
    const notificationsList = useSelector(sNotificationsList);
    return (
        <div className="notification-container">
            {notificationsList.map((notification) => (
                <Notification key={notification.id} notification={notification} />
            ))}
        </div>
    );
};
export default NotificationContainer;
Enter fullscreen mode Exit fullscreen mode

Awesome. We finished with notification development, now we can add this container to the layouts.

import React from 'react';
import { Outlet } from 'react-router-dom';
import NotificationContainer from '../../components/ui/notification/NotificationContainer.component';
import './emptyLayout.styles.scss';

const EmptyLayout = () => {
  return (
    <div className='emptyLayout'>
      <NotificationContainer />
      <Outlet />
    </div>
  );
};
export default EmptyLayout;
Enter fullscreen mode Exit fullscreen mode

And finally, we can use it on every page we will need it. For example, let's add a notification to our "registration" function so that our app can inform users about successes or errors.

const handleSubmit = async (e) => {
    e.preventDefault();
    const result = await registerNewUser({name, email, password});
    if (result.status === 'success') {
        navigate('/auth/login');
        dispatch(aPushNewNotification({
            type: 'success',
            text: 'You have successfully registered. Please login.',
        }));
    } else {
        dispatch(aPushNewNotification({
            type: 'error',
            text: result.message,
        }));
    }
}
Enter fullscreen mode Exit fullscreen mode

Restart the CMS frontend part, and try the registration process from the beginning to check the result.

React Notification system

Yes, it works, great job and let's move on.

2. Reusable Modal System

A modal is a UI component that appears as an overlay on top of the main content, typically used to display important information, forms, or user actions without navigating away from the current page. It often requires user interaction to close or proceed. We will create a reusable "modal" component so that we can use it whenever we need, as a confirmation popup for example, let's start:

  • add new data (modal status and modal type) into the state and new reducers to update those states into the "base.reducer.js" file;
modalStatus: false,
modalType: '',

case BASE_ACTION_TYPES.UPDATE_MODAL_STATUS:
    return { ...state, modalStatus: payload };
case BASE_ACTION_TYPES.UPDATE_MODAL_TYPE:
    return { ...state, modalType: payload };
Enter fullscreen mode Exit fullscreen mode
  • we will need two new selectors;
export const sModalStatus = (state) => state.base.modalStatus
export const sModalType = (state) => state.base.modalType
Enter fullscreen mode