Header

Toast notification system in a React/Redux application

Frontend and Tutorials

Whilst building Natterly, I wanted to build a simple toast notification system to display important alerts, without using the user's native notification system.

It looks a little something like this.

Screenshot of Natterly

In this post, we're going to be building out a very simple clone of this.

Actions & Reducer

Let's start off by creating the skeleton of the reducer. If no state is provided, we want to return an empty array, and by default we want to return the current state for all actions.

// src/reducers/toasts.js

export default function toasts(state = [], action) {
  const { payload, type } = action;

  switch (type) {
    default:
      return state;
  }
}

Now that we've got that set up, time to add it to our combined (or root) reducer.

// src/reducers/index.js

import { combineReducers } from "redux";
import toasts from "./toasts";

export default combineReducers({
  toasts
});

Now we want to write a function that'll take some options and spit out a new toast object, with an ID that is auto-incremented every time it is called.

// src/factories/createToast.js

let id = 0;

const defaultOptions = {
  color: "#6796e6"
};

export default function createToast(options) {
  return {
    ...defaultOptions,
    ...options,
    id: id++
  }
}

Notice how we've also provided some default options, which are merged into the options passed in. In your own application, you probably want to ensure that the options being provided are valid.

Now we want to create two actions. One to add a new toast notification to the store, and one to remove it by ID.

// src/constants/index.js

export const ADD_TOAST = "ADD_TOAST";
export const REMOVE_TOAST = "REMOVE_TOAST";
// src/actions/index.js

import createToast from "../factories/createToast";
import { ADD_TOAST, REMOVE_TOAST } from "../constants";

export function addToast(options = {}) {
  return {
    payload: createToast(options),
    type: ADD_TOAST
  };
}

export function removeToast(id) {
  return {
    payload: id,
    type: REMOVE_TOAST
  };
}

Now that we've got our two actions, let's create case statements for them in our reducer.

// src/reducers/toasts.js

import { ADD_TOAST, REMOVE_TOAST } from "../constants";

export default function toasts(state = [], action) {
  const { payload, type } = action;

  switch (type) {
    case ADD_TOAST:
      return [payload, ...state];

    case REMOVE_TOAST:
      return state.filter(toast => toast.id !== payload);

    default:
      return state;
  }
}

Cool, well that's pretty much everything Redux wise. Time to build our components.

Components & Styling

import PropTypes from "prop-types";
import React from "react";
import { bindActionCreators } from "redux";
import { connect } from "react-redux";
import Toast from "./Toast";
import { removeToast } from "../actions";

const Toasts = ({ actions, toasts }) => {
  const { removeToast } = actions;
  return (
    <ul className="toasts">
      {toasts.map(toast => {
        const { id } = toast;
        return (
          <Toast {...toast} key={id} onDismissClick={() => removeToast(id)} />
        );
      })}
    </ul>
  );
};

Toasts.propTypes = {
  actions: PropTypes.shape({
    removeToast: PropTypes.func.isRequired
  }).isRequired,
  toasts: PropTypes.arrayOf(PropTypes.object).isRequired
};

const mapDispatchToProps = dispatch => ({
  actions: bindActionCreators({ removeToast }, dispatch)
});

const mapStateToProps = state => ({
  toasts: state.toasts
});

export default connect(mapStateToProps, mapDispatchToProps)(Toasts);

All we're doing here is taking the list of toast notifications, rendering them, and passing a click handler function to each one. When clicked, we simply want to dispatch the removeToast action, with the ID of the toast we want to remove.

Now let's build out the Toast component.

// src/components/Toast.jsx

import PropTypes from "prop-types";
import React, { Component } from "react";

class Toast extends Component {
  render() {
    return (
      <li className="toast" style={{ backgroundColor: this.props.color }}>
        <p className="toast__content">
          {this.props.text}
        </p>
        <button className="toast__dismiss" onClick={this.props.onDismissClick}>
          x
        </button>
      </li>
    );
  }

  shouldComponentUpdate() {
    return false;
  }
}

Toast.propTypes = {
  color: PropTypes.string.isRequired,
  onDismissClick: PropTypes.func.isRequired,
  text: PropTypes.string.isRequired
};

export default Toast;

In our application toast notifications can't be changed once they are created, so we're just going to return false for shouldComponentUpdate to prevent unnecessary rendering when a new toast is added/removed from the collection.

That's pretty much it, now we just need to render out the Toasts component, wherever we want it to appear. In this case, we're going to render in the App component, because we want it to be displayed everywhere.

// src/components/App.jsx

import PropTypes from "prop-types";
import React, { Component } from "react";
import { bindActionCreators } from "redux";
import { connect } from "react-redux";
import Toasts from "./Toasts";
import { addToast } from "../actions";

class App extends Component {
  constructor(props) {
    super(props);
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    const { addToast } = this.props.actions;
    addToast({ text: "Hello, World!" });
  }

  render() {
    return (
      <main>
        <section>
          <h1>It's getting toasty!</h1>
          <p>Click the button below to dispatch a toast notification.</p>
          <button onClick={this.handleClick}>Dispatch</button>
        </section>
        <Toasts />
      </main>
    );
  }
}

App.propTypes = {
  actions: PropTypes.shape({
    addToast: PropTypes.func.isRequired
  }).isRequired
};

const mapDispatchToProps = dispatch => ({
  actions: bindActionCreators({ addToast }, dispatch)
});

export default connect(null, mapDispatchToProps)(App);

Screenshot of unstyled notifications

It works, but it looks revolting, let's sort that out with a little bit of styling.

.toast {
  align-items: flex-start;
  border-radius: 4px;
  color: #ffffff;
  display: flex;
  padding: 16px;
}

.toast:not(:last-child) {
  margin: 0 0 12px;
}

.toast__content {
  flex: 1 1 auto;
  margin: 0 12px 0 0;
  overflow: hidden;
  text-overflow: ellipsis;
}

.toast__dismiss {
  -webkit-appearance: none;
  -moz-appearance: none;
  background: transparent;
  border: 0;
  color: inherit;
  cursor: pointer;
  display: block;
  flex: 0 0 auto;
  font: inherit;
  padding: 0;
}

.toasts {
  bottom: 24px;
  position: fixed;
  right: 24px;
  width: 240px;
}

Screenshot of styled notifications

It still looks dreadful, but they're starting to look like actual toast notifications now!

If you found this post helpful, please consider checking out DeployHQ, a service designed to help you automate the deployment of your projects. We’ve even written a guide to help you automatically deploy your React application in less than 10 minutes.

A little bit about the author

Rob is the Product Development Manager at aTech Media. He is absurdly tall with a passion for CSS, JavaScript, burgers, and rugby.

Tree

Proudly powered by Katapult. Running on 100% renewable energy.