Create stacked snack bar in Reactjs

Create stacked snack bar component in Reactjs.

This question was asked to me in Razorpay’s frontend interview for SDE2. The question was divided into two parts

In the first part I had to implement the core feature of creating the snackbar where the snackbar can be closed and can also be auto closed.

In the second part, make it stacked where the next snackbar will be placed below the previous one. This was nice-to-have feature, implementing would have provided additional points in the interview.

Stacked snackbar in React

Let us try to solve the problem with the same order as it was asked.

Create an auto-closing snackbar in Reactjs

A snackbar is a toast component that popups after certain action to notify the user about the actions status, whether it is completed, failed, or is in progress.

Based on this definition and the requirements, we have to create a component that will support:

  • variants – Different color options to represent the type of the snackbar.
  • message – What message it will display.
  • autoCloseDuration – Duration after which it will auto close.

We will create a component SnackBarContainer that will hold the state of the snackbar which will act as a container and then a view component that will render the snackbar.

In the SnackBarContainer, we will maintain the state for the array of snackbar objects and loop through these array of objects to render the snackbars.

import { useEffect, useState } from "react";
import "./styles.css";

export default function SnackbarContainer() {
  const [snackBar, setSnackBar] = useState([]);

  const addSnackBar = () => {
    setSnackBar((existingSnackBars) => {
      return [
        ...existingSnackBars,
        {
          id: Date.now(),
          message: `Hello ${Date.now()}`,
        },
      ];
    });
  };

  const removeSnackBar = (id) => {
    setSnackBar((existingSnackBars) => {
      return existingSnackBars.filter((e) => e.id !== id);
    });
  };

  return (
    <div className="App">
      <button onClick={addSnackBar}>Add</button>
      {snackBar.map((e) => (
        <Snackbar key={e.id} {...e} onClose={removeSnackBar} />
      ))}
    </div>
  );
}

Here we have two methods addSnackBar that will add a new snackbar with a random id. We have used Date.now() that returns the current timestamp in milliseconds as the unique identifier. In the removeSnackBar we remove the snackbar with the given id.

In the snackbar view component, we pass all the props of the snackbar along with the onClose callback function that will be invoked when the user manually closes the snackbar or it auto-closes.

const AUTO_CLOSE_DURATION = 3500;
const Snackbar = ({ message, autoCloseDuration, variant, onClose, id }) => {
  useEffect(() => {
    let autoCloseTimerId = setTimeout(() => {
      onClose?.(id);
    }, autoCloseDuration || AUTO_CLOSE_DURATION);

    return () => clearTimeout(autoCloseTimerId);
  }, []);

  return (
    <div className="snackbar">
      <div>{message}</div>
      <div onClick={() => onClose(id)}>X</div>
    </div>
  );
};

We have used default AUTO_CLOSE_DURATION as fallback and we are not handling the variant, we will handle it in the stacked version.

We can add simple styling to the snackbar to display it on the right top corner with a default background color.

.snackbar {
  position: fixed;
  top: 10px;
  right: 10px;
  display: inline-flex;
  align-items: center;
  gap: 8px;
  justify-content: center;
  padding: 10px;
  border-radius: 4px;
  background: gainsboro;
}

In the SnackBarContainer we have add button, clicking on which will add new snackbar to the state and it will render, that will auto close after 3500 milliseconds.

A simple autoclosing snackbar in Reactjs

We have created a basic auto-closing snackbar but there are two limitations to it,

  • We cannot reuse throughout the application, currently it is in the form of single component.
  • We cannot stack it.

Both the issues can be tackled, if we can centralize the snackbar creation and removal so that there is only single source of truth throughout the codebase making it easier to handle the snackbars and also stack them.

We can do so by creating a snackbar context that will act as centralized single source to manage the snackbars.

Creating stacked snackbars in Reactjs

Create a snackbar context that will take the maxSnack as input that will be the number of snackbars that can be stacked.

<SnackbarProvider maxSnack={3}>
    <App />
</SnackbarProvider>

This will wrap the entire application inside it making the snackbar available globally within the codebase. We can will expose a hook useSnackbar that will have the addSnackbar method that you can use to add a snackbar from anywhere in the code.

Below is the complete code for the snackbar context.

import {
  createContext,
  useMemo,
  useContext,
  useState,
  useCallback,
} from "react";

const SnackbarContext = createContext();

export const SnackbarProvider = ({ children, maxSnack = 3 }) => {
  const [snackbars, setSnackbars] = useState([]);

  const addSnackbar = useCallback(({ variant = "success" }) => {
    const id = Date.now();
    setSnackbars((prev) => [...prev, { message: `Hello: ${id}`, id, variant }]);
  }, []);

  const removeSnackbar = useCallback((id) => {
    setSnackbars((prev) => prev.filter((snackbar) => snackbar.id !== id));
  }, []);

  const value = useMemo(
    () => ({
      maxSnack,
      addSnackbar,
      removeSnackbar,
      snackbars,
    }),
    [maxSnack, addSnackbar, removeSnackbar, snackbars]
  );

  return (
    <SnackbarContext.Provider value={value}>
      {children}
      <Snackbar />
    </SnackbarContext.Provider>
  );
};

export const useSnackbar = () => {
  const context = useContext(SnackbarContext);

  if (!context) {
    throw new Error(
      "useSnackbarProvider must be used within a `SnackbarProvider`"
    );
  }

  return context;
};

Here we have moved the state inside the context provider and added two methods addSnackbar and removeSnackbar to handle snackbar.

We can now update the Snackbar component to render snackbars.

import cx from "classnames";

const Snackbar = () => {
  const { snackbars, removeSnackbar, maxSnack } = useSnackbar();
  const maxSnackBarsToShow = snackbars.slice(0, maxSnack);

  return maxSnackBarsToShow.map((e, i) => (
    <SnackbarInner
      key={e.id}
      {...e}
      onClose={removeSnackbar}
      yAxisDisplacement={i * 40}
    />
  ));
};

const AUTO_CLOSE_DURATION = 3500;
const SnackbarInner = ({
  message,
  variant,
  onClose,
  id,
  yAxisDisplacement,
  autoCloseDuration
}) => {
  useEffect(() => {
    let autoCloseTimerId = setTimeout(() => {
      onClose?.(id);
    }, autoCloseDuration || AUTO_CLOSE_DURATION);

    return () => clearTimeout(autoCloseTimerId);
  }, []);

  return (
    <div
      className={cx("snackbar", [variant])}
      style={{
        transform: `translateY(${yAxisDisplacement}px)`,
      }}
    >
      <div>{message}</div>
      <div onClick={() => onClose(id)}>X</div>
    </div>
  );
};

In this we have used the classnames package to dynamically add the CSS classes and added the extra prop yAxisDisplacement that stacks the snackbars below each other, by displacing them 40px based on the order of their rendering.

.snackbar.error {
  background: red;
}

.snackbar.success {
  background: greenyellow;
}

You can update the logic to get the actually height of the snackbar or update the snackbar to have consistent height and trim the extra content and show it in tooltip, it is upto you.

Complete code for the stacked snackbars

import {
  createContext,
  useMemo,
  useContext,
  useState,
  useCallback,
  useEffect,
} from "react";
import cx from "classnames";
import "./styles.css";

const Snackbar = () => {
  const { snackbars, removeSnackbar, maxSnack } = useSnackbar();
  const maxSnackBarsToShow = snackbars.slice(0, maxSnack);

  return maxSnackBarsToShow.map((e, i) => (
    <SnackbarInner
      key={e.id}
      {...e}
      onClose={removeSnackbar}
      yAxisDisplacement={i * 40}
    />
  ));
};

const AUTO_CLOSE_DURATION = 3500;
const SnackbarInner = ({
  message,
  variant,
  onClose,
  id,
  yAxisDisplacement,
  autoCloseDuration,
}) => {
  useEffect(() => {
    let autoCloseTimerId = setTimeout(() => {
      onClose?.(id);
    }, autoCloseDuration || AUTO_CLOSE_DURATION);

    return () => clearTimeout(autoCloseTimerId);
  }, []);

  return (
    <div
      className={cx("snackbar", [variant])}
      style={{
        transform: `translateY(${yAxisDisplacement}px)`,
      }}
    >
      <div>{message}</div>
      <div onClick={() => onClose(id)}>X</div>
    </div>
  );
};

const SnackbarContext = createContext();

export const SnackbarProvider = ({ children, maxSnack = 3 }) => {
  const [snackbars, setSnackbars] = useState([]);

  const addSnackbar = useCallback(({ variant = "success" }) => {
    const id = Date.now();
    setSnackbars((prev) => [...prev, { message: `Hello: ${id}`, id, variant }]);
  }, []);

  const removeSnackbar = useCallback((id) => {
    setSnackbars((prev) => prev.filter((snackbar) => snackbar.id !== id));
  }, []);

  const value = useMemo(
    () => ({
      maxSnack,
      addSnackbar,
      removeSnackbar,
      snackbars,
    }),
    [maxSnack, addSnackbar, removeSnackbar, snackbars]
  );

  return (
    <SnackbarContext.Provider value={value}>
      {children}
      <Snackbar />
    </SnackbarContext.Provider>
  );
};

export const useSnackbar = () => {
  const context = useContext(SnackbarContext);

  if (!context) {
    throw new Error(
      "useSnackbarProvider must be used within a `SnackbarProvider`"
    );
  }

  return context;
};

Testcase

We can now trigger the snackbar for any component inside the codebase. Lets add this test logic in the App.jsx file.

import { useSnackbar } from "./SnackbarContext";

const variants = ["error", "success", "default"];
const MAX = 3;
const MIN = 1;

export default function App() {
  const { addSnackbar } = useSnackbar();

  const onClickHandler = () => {
    const random = Math.floor(Math.random() * (MAX - MIN + 1)) + MIN;
    const variant = variants[random - 1];
    addSnackbar({ variant });
  };

  return (
    <div className="App">
      <button onClick={onClickHandler}>Add</button>
    </div>
  );
}

This will randomly choose a variant and then add them to the stack.

Stacked snackbar in React