DEV Community

Cover image for Make your own Snackbars using React + Redux and Styled-Components
Thinh Nguyen
Thinh Nguyen

Posted on

Make your own Snackbars using React + Redux and Styled-Components

What is it? πŸ€”

A Snackbar is a UI component that provides the user visual feedback on an event within the app without interrupting the user experience. This is typically shown as a message box that informs the user of an action being performed or that will be performed by the app.

Snackbars typically behave in the following manner:

  • They provide information about an app process via text.
  • They will disappear on their own after a certain time.
  • They should not interrupt the user from anything else.

That being said, let's get started with a fresh create-react-app.


Setting Up The Redux Store πŸ’Ύ

For now, we'll create a simply button in our App component to trigger the Snackbar via react-redux. The setup for the redux store is very straightforward, with all the actions and reducers in the same redux folder as follows.

/* ---- redux/actions.js ---- */

export const toggleSnackbarOpen = (message) => ({
  type: "TOGGLE_SNACKBAR_OPEN",
  message,
});

export const toggleSnackbarClose = () => ({
  type: "TOGGLE_SNACKBAR_CLOSE",
});

Enter fullscreen mode Exit fullscreen mode

For now, we just want to be able to pass a message in our dispatch to be rendered in the Snackbar, other parameters can also be added such as a timer or even the variation of snackbar being rendered i.e. success, warning, or informative, but for now we'll stick with the basics.

/* ---- redux/reducers.js ---- */

const initialState = {
  toggleSnackbar: false,
  snackbarMessage: null,
};

export default function reducer(state = initialState, action) {
  switch (action.type) {
    case "TOGGLE_SNACKBAR_OPEN": {
      return {
        ...state,
        toggleSnackbar: true,
        snackbarMessage: action.message,
      };
    }

    case "TOGGLE_SNACKBAR_CLOSE": {
      return {
        ...state,
        toggleSnackbar: false,
        snackbarMessage: null,
      };
    }

    default: {
      return state;
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

In our reducers' initial state we would need a message and the boolean state of the Snackbar. We can also include different states for different types of messages like warningMessage for a toggleWarningSnackbar state.

/* ---- redux/store.js ---- */

import { createStore } from "redux";
import reducer from "./reducers";

const config =
  window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__();

export default function configureStore(initialState) {
  const store = createStore(reducer, initialState, config);
  return store;
}

Enter fullscreen mode Exit fullscreen mode

And of course, we create the redux store and configure it, and then we connect the store to the App with the Provider as follows:

/* ---- index.js ---- */

import React from "react";
import ReactDOM from "react-dom";
import App from "./App";

import { Provider } from "react-redux";
import configureStore from "./redux/store";

const store = configureStore();

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById("root")
);

Enter fullscreen mode Exit fullscreen mode

The Snackbar Component 🍫

For this example, we only want our Snackbar to display a message and fade itself away after an event is fired, but it will also allow the user to dismiss the Snackbar altogether. We will also add a timeout variable as a prop to this component to define when the notification will disappear.

import React, { useEffect } from "react";
import styled, { keyframes } from "styled-components";
import { useDispatch, useSelector } from "react-redux";
import { toggleSnackbarClose } from "../redux/actions";
import { FiX } from "react-icons/fi";

const Snackbar = ({ timeout }) => {
  const dispatch = useDispatch();

  // select the UI states from the redux store
  const SHOW = useSelector((state) => state.toggleSnackbar);
  const MESSAGE = useSelector((state) => state.snackbarMessage);

  // convert the timeout prop to pass into the styled component
  let TIME = (timeout - 500) / 1000 + "s";

  let TIMER;
  function handleTimeout() {
    TIMER = setTimeout(() => {
      dispatch(toggleSnackbarClose());
    }, timeout);
  }

  function handleClose() {
    clearTimeout(TIMER);
    dispatch(toggleSnackbarClose());
  }

  useEffect(() => {
    if (SHOW) {
      handleTimeout();
    }
    return () => {
      clearTimeout(TIMER);
    };
  }, [SHOW, TIMER]);

  return (
    SHOW && (
      <Container time={TIME}>
        <p>{MESSAGE}</p>
        <Button onClick={handleClose}>
          <FiX />
        </Button>
      </Container>
    )
  );
};

const fadein = keyframes`
    from {
      bottom: 0;
      opacity: 0;
    }
    to {
      bottom: 1rem;
      opacity: 1;
    }
`;

const fadeout = keyframes`
    from {
      bottom: 1rem;
      opacity: 1;
    }
    to {
      bottom: 0;
      opacity: 0;
    }
`;

const Container = styled.div`
  position: fixed;
  z-index: 1000;
  bottom: 1rem;
  left: 50%;
  transform: translateX(-50%);
  height: auto;
  padding: 0.625rem 1rem;
  border-radius: 0.75rem;
  border: transparent;
  background-color: hsl(200, 100%, 65%);
  color: white;
  box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23);

  display: flex;
  justify-content: center;
  align-items: center;

  animation: ${fadein} 0.5s, ${fadeout} 0.5s ${(props) => props.time};
`;

const Button = styled.button`
  display: flex;
  justify-content: center;
  align-items: center;
  margin-left: 0.875rem;
  padding: 0;
  margin-left: 1rem;
  height: 1.75rem;
  width: 1.75rem;
  text-align: center;
  border: none;
  border-radius: 50%;
  background-color: transparent;
  color: white;
  cursor: pointer;

  &:hover {
    background-color: hsl(200, 100%, 60%);
  }
`;

export default Snackbar;

Enter fullscreen mode Exit fullscreen mode

To make the Snackbar disappear on its own once toggled we use setTimeout to trigger another dispatch to close the Snackbar according to the value of the timeout prop. You will notice that 0.5s was shaved off to the TIME variable to allow our nice fadeOut animation to take place, when it is passed as a prop to our Container component. Note that the keyframes animations must take precedence before being called into the animation CSS property

Additionally, we have another button within the Snackbar that displays alongside the message that can close the Snackbar.


The App πŸ–₯️

With reusability in mind, we want to be able to activate the Snackbar just by simply importing its component and its action dispatcher to any view.

/* ---- App.js ---- */

import React from "react";
import GlobalStyles from "./components/GlobalStyles";
import styled from "styled-components";
import Snackbar from "./components/Snackbar";
import { useDispatch, useSelector } from "react-redux";
import { toggleSnackbarOpen } from "./store/actions";

const App = () => {
  const dispatch = useDispatch();

  return (
    <>
      <GlobalStyles />
      <Wrapper>
        <Button
          onClick={() => {
            dispatch(toggleSnackbarOpen("I'm a Snackbar!"));
          }}
        >
          Click Me!
        </Button>
        <Snackbar timeout={3000} />
      </Wrapper>
    </>
  );
};

const Wrapper = styled.div`
  height: 100vh;
  background: #fffc;
  display: flex;
  justify-content: center;
  align-items: center;
`;

const Button = styled.button`
  padding: 0.5rem 1rem;
  font-size: 1.3rem;
  border-radius: 0.5rem;
  outline: none;
  border: none;
  background: lightblue;
  cursor: pointer;
  box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14),
    0 3px 1px -2px rgba(0, 0, 0, 0.12), 0 1px 5px 0 rgba(0, 0, 0, 0.2);

  &:hover {
    background: lightgoldenrodyellow;
  }
`;

export default App;

Enter fullscreen mode Exit fullscreen mode

And just like that, we have a working (albeit basic) Snackbar that can be re-used in any other component! The code shown so far can also be viewed in this Code Sandbox snippet:


✨ But Wait, There's More! ✨

There are a lot of cool features to add in a Snackbar, such as its anchoring position or having Snackbar variations, for the full specifications, you should definitely check out Material Design's page here.

If you are interested in adding some of those features, please feel free to check out my take on this over here:

GitHub logo g-thinh / simple-snackbar

Re-created Material UI's snackbar component

Happy Coding 😊

Top comments (2)

Collapse
 
adouz profile image
Abdellatif Douz

what about if you have multiple message that you want to show??

Collapse
 
gthinh profile image
Thinh Nguyen • Edited

Hi there! To handle multiple messages, you would need to update your redux store to manage an array of message objects. You would then map() the messages array instead.

Hope that helps!