How to show in-app messages using Material-UI in a React web app (Technical)

Author: | Created on: Feb 26th 2019 | Last edited on: 2 months ago


How to show in-app messages using Material-UI in a React web app

Kelly Burke
Apr 27, 2018

Async

In some situations, your web app needs to show an informational message to tell users whether an event was successful or not. For example, a “Success” message after a user clicks a button and successfully completes some action.

In this tutorial, I’ll show you how to create a simple component for informational, in-app messages with React and Material-UI. We’ll call it a Notifier component.

Here are the main sections of this tutorial:

  • Getting started
  • Notifier component
  • Import Notifier component to Index page
  • Testing

If you find this article useful, consider starring our Github repo launch and checking out our book launch where we cover this and many other topics in detail.


Getting started

For this tutorial, I’ve created a simple web app for you to follow. We’ll use code located in the tutorials/4-start launch folder of our builderbook repo launch .

If you don’t have time to run the app locally, I deployed this example app here launch .

To run the app locally:

  • Clone the builderbook repo to your local machine with: git clone git@github.com:builderbook/builderbook.git
  • Inside the 4-start folder, run yarn or npm install to install all packages listed in package.json.
  • Run yarn dev to start the app.

Index page

On your browser, go to http://localhost:3000 launch . This is our Index page, which has the / route. Next.js provides automatic routing for pages located in a /pages folder. The name of each page file becomes that page’s route.

Our Index page is a simple page component that renders a form, an input, and a button (more explanation below).

Async

Here’s the code for our Index page at pages/index.js:

import React from 'react';
import Head from 'next/head';
import TextField from 'material-ui/TextField';
import Button from 'material-ui/Button';
import withLayout from '../lib/withLayout';

class Index extends React.Component {
  render() {
    return (
      <div style={{ padding: '10px 45px' }}>
        <Head>
          <title>Notifier component</title>
          <meta
            name="description"
            content="How to show informational messages using Material-UI in a React web app"
          />
        </Head>
        <form>
          <p> What is 2+2? </p>
          <TextField
            type="number"
            label="Type your answer"
            style={{
              font: '15px Muli',
              color: '#222',
              fontWeight: '300',
            }}
          />
          <p />
          <Button
            variant="raised"
            color="primary"
            type="submit"
          >
            Submit
          </Button >
        </form>
      </div >
    );
  }
}

export default withLayout(Index);

A few notes:

  • We could have defined this page as a stateless functional component, since it has no state, lifecycle hooks, or refs ( read more launch about when to use stateless functional components versus React ES6 classes). You’ll see this Eslint warning: Component should be written as a pure function. However, the final Index page that we write in this tutorial will have ref. Hence, we wrote this initial Index page as a child of ES6 class launch using extends launch .
  • We imported Head launch from Next.js in order to customize the Head element of the page. Inside Head, we added a page title and meta description for proper indexing by search engine bots (good for SEO). The text inside title displays on your browser tab:
Async
  • We used Material-UI’s TextField launch and Button launch components, which render into input and button HTML elements, respectively.
  • We wrapped our page with a withLayout higher-order component. Our app uses Next.js, and withLayout ensures that server-side rendering works properly for Material-UI inside our React-Next app. withLayout also adds our Header component (located at components/Header.js) to each page that withLayout wraps. Server-side rendering is not necessary for Material-UI or React, but it is a main feature of Next.js. We discussed server-side vs. client-side rendering launch in React apps in another tutorial.

We are done describing our initial Index page. Now let’s discuss the Notifier component that we will later import into the Index page to show informational messages to our web app users.


Notifier component

Let’s start by defining the Notifier component. We define Notifier as a React.Component instead of a stateless function, because Notifier will have state, one lifecycle method, and a few event handling functions.

class Notifier extends React.Component {}

For our informational messages, we will use Material-UI’s Snackbar launch . Check out examples launch of using Snackbar on the official Material-UI site.

Here’s a high-level outline of our Notifier component:

import React from 'react';
import Snackbar from 'material-ui/Snackbar';
class Notifier extends React.Component {
  // 1. set the Notifier's initial state
  // 2. define a function to open Snackbar and show a message
  // 3. define a function to close Snackbar when a user clicks away
render() {
  // 4. show a message to users
return (
      <Snackbar
        // 5. write styles and pass props to the Snackbar component
      />
    );
  }
}
export default Notifier;

Create a Notifier.js file inside the /components folder of 4-start. Add the above high-level outline to this file. Below, we will replace the numbered comments with code.

  1. We will use the open and message props of Material-UI’s Snackbar for the state of our Notifier. Check the full list of props launch for Snackbar.

Initially, our Notifier should be in a closed state with an empty string as a message. We define the Notifier's initial state as:

state = {
    open: false,
    message: '',
  };
  1. Now let’s write a function that updates the state of the Notifier component. The function will change the value of the open prop to true and set the value of the message prop to a non-empty string. Let’s call this function openSnackbar().

Before we can execute openSnackbar(), our Notifier component needs to be mounted in the DOM. Thus, we put the openSnackbar() function into a componentDidMount lifecycle method that executes right after the Notifier component mounts in the DOM.

In order to access the openSnackbar() function anywhere in our app, we have to set its value to another function that is available outside of the Notifier component. Hence, we write let openSnackbarFn above class Notifier extends React.Component.

Putting these pieces together:

...
let openSnackbarFn
...
componentDidMount() {
    openSnackbarFn = this.openSnackbar;
  }

Now let’s define the openSnackbar() function. This function will update the open and message properties of our Notifier’s state. Once the state is updated, the Notifier component will get re-rendered to show a message (open:true displays the Snackbar, and message:message sets the message).

openSnackbar = ({ message }) => {
    this.setState({
      open: true,
      message,
    });
  };

Inside this.setState, we could have written message as message:message. Instead, we used ES6 shorthand syntax launch (enforced by Eslint) to make the code more concise.

  1. When a user clicks outside of the Snackbar area, the Snackbar should close. The Snackbar prop onClose is responsible for this behavior. Let’s write a function called handleSnackbarClose() that sets open to false and message to an empty string.
handleSnackbarClose = () => {
    this.setState({
      open: false,
      message: '',
    });
  };
  1. Finally, let’s write code for our Notifier component to render the Snackbar component with all necessary props.

In addition to the message, onClose, and open props described above, we’ll add the following props to our Snackbar component:

  • anchorOrigin: specifies the Snackbar location
  • autoHideDuration: specifies the Snackbar duration in milliseconds
  • SnackbarContentProps: binds the Snackbar to an element inside the DOM that contains message; in our case, the element has the id snackbar-message-id, and the Snackbar will display text from this element.

Here is the render() method of our Notifier component:

render() {
    const message = (
      <span 
        id="snackbar-message-id"
        dangerouslySetInnerHTML={{ __html: this.state.message }} />
    );
return (
      <Snackbar
        anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
        message={message}
        autoHideDuration={3000}
        onClose={this.handleSnackbarClose}
        open={this.state.open}
        SnackbarContentProps={{
          'aria-describedby': 'snackbar-message-id',
        }}
      />
    );
  }

In the span element, we could have written message={this.state.message}, but instead we wrote dangerouslySetInnerHTML={{ __html: this.state.message }} . This allows us to add HTML code to the Snackbar’s message prop. For instance, you may want to show a hyperlink to users. Read more about launch using dangerouslySetInnerHTML in React.

After putting the code from steps 1–4 together, here’s our final Notifier component:

import React from 'react';
import Snackbar from 'material-ui/Snackbar';

let openSnackbarFn;

class Notifier extends React.Component {
  state = {
    open: false,
    message: '',
  };

  componentDidMount() {
    openSnackbarFn = this.openSnackbar;
  }

  openSnackbar = ({ message }) => {
    this.setState({
      open: true,
      message,
    });
  };

  handleSnackbarClose = () => {
    this.setState({
      open: false,
      message: '',
    });
  };

  render() {
    const message = (
      <span
        id="snackbar-message-id"
        dangerouslySetInnerHTML={{ __html: this.state.message }}
      />
    );

    return (
      <Snackbar
        anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
        message={message}
        autoHideDuration={3000}
        onClose={this.handleSnackbarClose}
        open={this.state.open}
        SnackbarContentProps={{
          'aria-describedby': 'snackbar-message-id',
        }}
      />
    );
  }
}

export function openSnackbar({ message }) {
  openSnackbarFn({ message });
}

export default Notifier;

Important note: notice how we exported our openSnackbar() function in addition to Notifier component. We will import both openSnackbar() and Notifier into our Index page.


Import Notifier component to Index page

Let’s go back to our Index page, where we will import our Notifier component and openSnackbar() function.

Without triggering the openSnackbar() function, our Notifier component will always stay in its initial closed state with an empty string as a message. We need to execute openSnackbar() after a user submits the form by clicking the button on our Index page. Let’s define a showNotifier() function that does exactly that.

showNotifier Function

We will call showNotifier() inside the form element. We’ll make showNotifier() execute when a user enters a number on the form and clicks the “Submit” button.

Here’s the current form on our Index page:

<form>
  <p> What is 2+2? </p>
  <TextField
   type="number"
   label="Type your answer"
   style={{
     font: '15px Muli',
     color: '#222',
     fontWeight: '300',
   }}
  />
  <p />
  <Button
   variant="raised"
   color="primary"
   type="submit"
  >
   Submit
  </Button >
</form>

Let’s make two modifications:

  1. To call showNotifier() when the form is submitted, we use JavaScript’s onsubmit Event launch :
<form onSubmit={this.showNotifier}>
  1. A user will enter a number inside TextField. In order for our showNotifier() function to access the value of TextField, we add React’s ref attribute launch to TextField.

There are two ways to get the value of TextField: with this.state and with ref. We chose ref, since it's more concise than this.state. Here’s an explanation launch of writing with this.state, and here’s more info launch about using ref in React.

inputRef={(elm) => {
  this.answerInput = elm;
}}

Now let’s define the showNotifier() function. Here’s the high-level outline for showNotifier():

showNotifier = (event) => {
  // 1. define data that gets submitted on the form as 'answer'
  // 2. if no answer is submitted, show 'Empty field. Enter a number'
  // 3. if '4' is submitted, show 'Correct answer!'; else, show 'Incorrect answer.'
}

Below, we’ll write code for each of the three comments above.

  1. We define answer as:
const answer = (this.answerInput && this.answerInput.value) || null;

This line of code says that IF answerInput exists (meaning the input element is added to the DOM), THEN answer equals the value of answerInput, which is accessed with answerInput.value.

IF answerInput does not exist, THEN the entire condition in parentheses is false and answer equals null.

  1. If a user does not enter an answer on our form but clicks the “Submit” button, we will show this message: Empty field. Enter a number.
if (this.answerInput && !answer) {
      openSnackbar({ message: 'Empty field. Enter a number.' });
  1. If a user enters 4 and clicks the “Submit” button, then our openSnackbar() function will run and show this message: Correct answer!. Otherwise, it will show Incorrect answer.

We use parseInt(answer, 10) to parse answer, which is a string, and return an integer. The parameter 10 specifies that the integer is in decimal format.

if (answer && parseInt(answer, 10) === 4) {
      openSnackbar({ message: 'Correct answer!' });
    } else if (answer && parseInt(answer, 10) !== 4) {
      openSnackbar({ message: 'Incorrect answer.' });
    }

Let’s put together the code from steps 1–3 above for our showNotifier function. We’ll place the code right under the line class Index extends React.Component:

class Index extends React.Component {
  showNotifier = (event) => {
    event.preventDefault();

    const answer = (this.answerInput && this.answerInput.value) || null;

    if (this.answerInput && !answer) {
      openSnackbar({ message: 'Empty field. Enter a number.' });
    }

    if (answer && parseInt(answer, 10) === 4) {
      openSnackbar({ message: 'Correct answer!' });
    } else if (answer && parseInt(answer, 10) !== 4) {
      openSnackbar({ message: 'Incorrect answer.' });
    }
  }

You’ll notice that we added a line event.preventDefault();. This will prevent our form element from its default behavior of sending form data to a server launch .

Now we have all the code for our final Index page:

import React from 'react';
import Head from 'next/head';
import TextField from 'material-ui/TextField';
import Button from 'material-ui/Button';
import Notifier, { openSnackbar } from '../components/Notifier';
import withLayout from '../lib/withLayout';

class Index extends React.Component {
  showNotifier = (event) => {
    event.preventDefault();

    const answer = (this.answerInput && this.answerInput.value) || null;

    if (this.answerInput && !answer) {
      openSnackbar({ message: 'Empty field. Enter a number.' });
    }

    if (answer && parseInt(answer, 10) === 4) {
      openSnackbar({ message: 'Correct answer!' });
    } else if (answer && parseInt(answer, 10) !== 4) {
      openSnackbar({ message: 'Incorrect answer.' });
    }
  }

  render() {
    return (
      <div style={{ padding: '10px 45px' }}>
        <Head>
          <title>Notifier component</title>
          <meta
            name="description"
            content="How to show informational messages using Material-UI in a React web app"
          />
        </Head>
        <br />
        <Notifier />
        <form onSubmit={this.showNotifier}>
          <p> What is 2+2? </p>
          <TextField
            inputRef={(elm) => {
              this.answerInput = elm;
            }}
            type="number"
            label="Type your answer"
            style={{
              font: '15px Muli',
              color: '#222',
              fontWeight: '300',
            }}
          />
          <p />
          <Button
            variant="raised"
            color="primary"
            type="submit"
          >
            Submit
          </Button >
        </form>
      </div >
    );
  }
}

export default withLayout(Index);

Testing

Let’s test that our Notifier works as expected. Run the app locally with yarn dev and navigate to http://localhost:3000 launch . If you aren’t running the app, go to the one I deployed: https://notifier.builderbook.org launch .

First, click “Submit” without adding anything in the text field.

Async

Next, add the number 4 and click “Submit”.

Async

Now add any other number and click “Submit”.

Async

Remember that we wrote code to close the Snackbar when a user clicks away from it (we wrote a handleSnackbarClose() function and passed it to the onClose prop of the Snackbar). After seeing the Snackbar, click anywhere besides the Snackbar itself on your screen. The Snackbar should close immediately.

A nice feature of Material-UI is mobile optimization. We don’t have to write extra code for our informational message to look good on mobile devices. See for yourself by going to Chrome’s DevTools and changing the view from desktop to mobile. Our message appears across the top of the screen:

Async

Woohoo! You’ve successfully added an informational, in-app message to a React web app! Your final code should match the code in the tutorials/4-end launch folder of our builderbook repo launch .

Customize Notifier component

Now that you have a working Notifier component, let’s see how we can modify the UX by changing props of Material-UI’s SnackBar component. Here’s the full list launch of props you can use.

First, let’s change the duration of the Snackbar. Insider your Notifier component, find the autoHideDuration prop. Change its value from 3000 to 1000 and compare. You’ll see that at 1000, the Snackbar closes more quickly.

Second, let’s change the position of the Snackbar. Find the anchorOrigin prop and change its values from top and right to bottom and left, respectively. Check where the Snackbar appears now:

Async

Finally, let’s make the Snackbar message include a hyperlink. Recall that we added dangerouslySetInnerHTML={{ __html: this.state.message }} to our message prop in the Snackbar so that we can write HTML inside of it.

Change the code for our Correct answer! and Incorrect answer. messages like this:

    if (answer && parseInt(answer, 10) === 4) {
      openSnackbar({ message: 'Correct answer! <a href="https://media.giphy.com/media/TdfyKrN7HGTIY/giphy.gif" target="_blank">Good job!</a>' });
    } else if (answer && parseInt(answer, 10) !== 4) {
      openSnackbar({ message: 'Incorrect answer. <a href="https://media.giphy.com/media/TPl5N4Ci49ZQY/giphy.gif" target="_blank">Try again.</a>' });
    }
  }

Now users will see the messages below. Notice the dark blue hyperlinks for text inside the <a> tags.

Async
Async

If you’re learning how to build web apps with JavaScript, check out our Github repo launch and our book launch , where we cover this and many other topics in detail.

If you're building a software product, check up our SaaS boilerplate launch and Async launch , our team communication framework and tool for small teams of software engineers).