How to build an Alert Component using Recoil

H
andrewgbliss
5 months ago

This article we will use Recoil to build an alert component using React and Recoil.

. . .

React state management is a broad topic, and choosing the right management tool is often difficult. React is known for its missing state management tool often leaving it to the developer to figure it out. However, over the years, some great tools have emerged giving the developer some powerful features.

This article we will use Recoil to build an alert component, but you can apply the same principles in any state management tool you choose.

If you want to just jump into the code here is the repo for the whole project:

https://github.com/EntryLevelDeveloperTraining/recoil-alerts

Setup

I will be starting the project from a starter project I created called, Next.js Tailwind CSS Starter.

You can find the code repo here:

https://github.com/EntryLevelDeveloperTraining/nextjs-tailwind-css-starter

So I am starting my project using Next.js and Tailwind CSS to easily get something off the ground without too much configuration. You can git clone this repo if you want a starting point.

Installation

Now that I have my starter project ready to go, all I need now is to install Recoil.

npm install recoil

Styles

Before we get into the creating of the component I want to setup my styles using Tailwind CSS. Here are my components in Tailwind that I will be using to show an Info, Error, and Warning alert.

.screen {
  @apply w-screen h-screen flex justify-center items-center bg-blue-200;
}
.paper {
  @apply bg-white shadow-2xl rounded-lg border p-10;
}
.h1 {
  @apply text-center pb-5 text-xl;
}
.alert-info {
  @apply bg-green-200 border-green-600 text-green-800;
}
.alert-error {
  @apply bg-red-200 border-red-600 text-red-800;
}
.alert-warning {
  @apply bg-yellow-200 border-yellow-600 text-yellow-800;
}
.btn-info {
  @apply bg-green-200 hover:bg-green-100 p-5 rounded text-black focus:outline-none;
}
.btn-error {
  @apply bg-red-200 hover:bg-red-100 p-5 rounded text-black focus:outline-none;
}
.btn-warning {
  @apply bg-yellow-200 hover:bg-yellow-100 p-5 rounded text-black focus:outline-none;
}

Nothing too difficult here. You can see I am defining my colors for the alert like this:

bg-red-200 border-red-600 text-red-800;

Where the background is a soft color, the border has more saturation, and the text has the most.

Recoil State

Now let’s create the Recoil state that will hold the text needed to show an alert, what type, and a timeout to close the alert after a certain amount of time passes.

import { atom, RecoilState } from 'recoil';
export interface AlertState {
  open: boolean;
  primary: string;
  secondary: string;
  type: string;
  timeout: number;
}
export const alertState: RecoilState<AlertState> = atom({
  key: 'alertState',
  default: {
    open: false,
    primary: '',
    secondary: '',
    type: 'info',
    timeout: 5000,
  },
});

I am also using Typescript to define what information I will hold in my Recoil Atom.

A Recoil Atom is the fundamental type in Recoil that you can use to store any type of state.

It keeps the same flow that native React Hooks provides, so it’s easy to implement into a React project flow.

I give it the key of alertState to identify that I will be using it for my alert component.

Alert Component

Now let’s talk about the alert component. I will need to break this into two parts. The first component will be the actual component to show the alert and the second component will be a container to know when to show the different types of alerts.

interface AlertProps {
  type?: string;
  icon?: React.ReactNode;
  primary: string;
  secondary?: string;
  onClose?: Function;
}
function Alert(props: AlertProps) {
  const { type, icon, primary, secondary, onClose } = props;
  return (
    <div
      className={`relative border-t-4 rounded-b px-4 py-3 shadow-md my-2 alert-${type}`}
      role="alert"
    >
      <div className="absolute top-0 right-0 p-2">
        <div className="cursor-pointer" title="Close" onClick={() => onClose()}>
          <CloseIcon />
        </div>
      </div>
      <div className="flex items-center">
        <div className="p-2 pr-4">{icon}</div>
        <div>
          <p className="font-bold">{primary}</p>
          <p className="text-sm">{secondary}</p>
        </div>
      </div>
    </div>
  );
}

The first component will be responsible for showing an icon, showing the primary and secondary text, and a close button. It will also reference the Tailwind CSS styled components I setup above.

function InfoAlert(props: AlertProps) {
  return <Alert {...props} type="info" icon={<InfoIcon />} />;
}
function ErrorAlert(props: AlertProps) {
  return <Alert {...props} type="error" icon={<ErrorIcon />} />;
}
function WarningAlert(props: AlertProps) {
  return <Alert {...props} type="warning" icon={<WarningIcon />} />;
}

Here I will setup some helper components that will reference the correct types and icons. I need to mention that I created the icons by going to https://heroicons.dev/

export function useAlert() {
  const [alert, setAlert] = useRecoilState<AlertState>(alertState);
  const showAlert = (props) => {
    setAlert({
      ...alert,
      ...props,
    });
  };
  useEffect(() => {
    if (alert?.open && alert?.timeout) {
      setTimeout(() => {
        showAlert({
          open: false,
        });
      }, alert?.timeout);
    }
  }, [alert?.open, alert?.timeout]);
  return { alert, showAlert };
}

I also setup a hook that uses the Recoil state and a helper function to set the state of the Recoil atom. It also uses a useEffect to setup a timeout, whenever it opens it will set the timeout to close the alert.

export default function Alerts() {
  const { alert, showAlert } = useAlert();
  const onClose = () => {
    showAlert({
      open: false,
    });
  };
  
  return (
    <Transition
      show={alert.open}
      enter="transition ease-out duration-500"
      enterFrom="transform opacity-0 scale-95"
      enterTo="transform opacity-100 scale-100"
      leave="transition ease-in duration-500"
      leaveFrom="transform opacity-100 scale-100"
      leaveTo="transform opacity-0 scale-95"
      className="fixed inset-x-0 top-0 w-2/3 md:w-1/3 mx-auto z-50"
    >
      {alert.type === 'info' && (
        <InfoAlert
          primary={alert.primary}
          secondary={alert.secondary}
          onClose={() => onClose()}
        />
      )}
      {alert.type === 'error' && (
        <ErrorAlert
          primary={alert.primary}
          secondary={alert.secondary}
          onClose={() => onClose()}
        />
      )}
      {alert.type === 'warning' && (
        <WarningAlert
          primary={alert.primary}
          secondary={alert.secondary}
          onClose={() => onClose()}
        />
      )}
    </Transition>
  );
}

Tailwind also has a transition library to make transitions a lot simpler to use with their classes. If you want to install the library to use the Transition component like I am doing here then you can run this install command:

npm install @headlessui/react

So above is the main Alert component that will know when to show what type of Alert. I used conditional statements to document better what the code is doing, however I could have used a map to show the component based on type. It’s also responsible for closing the alert.

App Component

In the Next.js _app.tsx component I need to add in some code for Recoil and my Alert component.

import '../styles/index.css';
import { RecoilRoot } from 'recoil';
import Alerts from '../components/core/Alerts';
function MyApp({ Component, pageProps }) {
  return (
    <RecoilRoot>
      <Component {...pageProps} />
      <Alerts />
    </RecoilRoot>
  );
}
export default MyApp;

Recoil will not work unless we wrap the app in a RecoilRoot component, and I also add in my Alert component to be able to use it anywhere in the app.

Home Page

In the home page index.tsx component I added some testing functionality to show the alerts in action.

import { useAlert } from '../components/core/Alerts';
function InfoButton() {
  const { showAlert } = useAlert();
  const onClick = (type: string) => {
    showAlert({
      open: true,
      primary: 'Message',
      secondary: 'This is the message for the alert',
      type,
    });
  };
  return (
    <button className="btn-info" onClick={() => onClick('info')}>
      Info Alert
    </button>
  );
}
function ErrorButton() {
  const { showAlert } = useAlert();
  const onClick = (type: string) => {
    showAlert({
      open: true,
      primary: 'Message',
      secondary: 'This is the message for the alert',
      type,
    });
  };
  return (
    <button className="btn-error" onClick={() => onClick('error')}>
      Error Alert
    </button>
  );
}
function WarningButton() {
  const { showAlert } = useAlert();
  const onClick = (type: string) => {
    showAlert({
      open: true,
      primary: 'Message',
      secondary: 'This is the message for the alert',
      type,
    });
  };
  return (
    <button className="btn-warning" onClick={() => onClick('warning')}>
      Warning Alert
    </button>
  );
}
export default function Home() {
  return (
    <div className="screen">
      <div className="paper">
        <div className="h1">Recoil Alerts</div>
        <div className="flex gap-2">
          <InfoButton />
          <ErrorButton />
          <WarningButton />
        </div>
      </div>
    </div>
  );
}

I setup three buttons when click it will show the an alert.

Conclusion

There are many state management tools for React, however I have found the Recoil is a great library to use when you want to keep the same API fundamentals as React Hooks, and not process too many renders.

There are also many Alert components you can use, however I find that using Tailwind CSS and styling your own components makes your pages stand out and aren’t cookie cutter.


H
andrewgbliss
5 months ago