javascript

How to Use the Material UI Button Component

A button is a commonly used component that adds interactivity to a UI. In this article, we’re going to learn how to easily create and customize buttons in Material UI.

The Material UI Button Component

We can use the Button component from Material UI to create buttons. It has a variant prop used to display a text, contained, or outlined button.

App.jsx
import { Box, Button, Stack } from '@mui/material'; export default function App() { return ( <Box> <Stack spacing={2} direction="row" > <Button variant="text">Text</Button> <Button variant="contained">Contained</Button> <Button variant="outlined">Outlined</Button> </Stack> </Box> ); }

Text Button

Text buttons are suitable for actions of low significance in an app, like the closing of a dialog. Setting the variant prop to text displays a text button.

App.jsx
<Button>Primary</Button> <Button disabled>Disabled</Button> <Button href="#text-buttons">Link</Button>
Creating text buttons in Material UI.

Contained Button

Contained buttons indicate the primary and essential actions in our apps. Setting the variant prop to contained displays a contained button.

JavaScript
<Button variant="contained">Contained</Button> <Button variant="contained" disabled> Disabled </Button> <Button variant="contained" href="#contained-buttons"> Link </Button>
Creating contained buttons in Material UI.

Outlined Button

Outlined buttons indicate actions of mid-level significance. They are a lower emphasis alternative to contained buttons and a higher emphasis alternative to text buttons. Setting the variant prop to outlined displays and outlined button.

JavaScript
<Button variant="outlined">Primary</Button> <Button variant="outlined" disabled> Disabled </Button> <Button variant="outlined" href="#outlined-buttons"> Link </Button>
Creating outlined buttons in Material UI.

Disabled Button Elevation

We can prevent a button from being clicked by setting the disableElevation prop to true.

JavaScript
<Button variant="contained" disableElevation > Elevation disabled </Button>
Disabling button elevation.

Handling Button Clicks in Material UI

We can assign a listener function to the onClick prop to perform an action when the button is clicked.

In the following example, we attach a listener that increments a count by one, to display the total number of times the button has been clicked.

JavaScript
import { Box, Button, Typography } from '@mui/material'; import { useState } from 'react'; export default function App() { const [count, setCount] = useState(0); return ( <Box sx={{ margin: 2 }}> <Button onClick={() => { setCount(count + 1); }} variant="contained" > Click me </Button> <Typography sx={{ marginTop: 1 }}>Count: {count}</Typography> </Box> ); }
Handling button clicks in Material UI.

Material UI Button Colors

We can use the color prop to apply a color from the theme palette.

JavaScript
<Button color="secondary">Secondary</Button> <Button variant="contained" color="success"> Success </Button> <Button variant="outlined" color="error"> Error </Button>
Applying color to buttons.

Custom Colors

The color prop only allows values from the theme palette. To apply a color not available in the theme, we can use custom CSS and the sx prop.

JavaScript
import { Stack, Button } from '@mui/material'; import { green } from '@mui/material/colors'; export default function App() { return ( <Stack spacing={2} direction="row" > <Button sx={{ backgroundColor: green[500], '&:hover': { backgroundColor: green[700] }, }} variant="contained" > Primary </Button> <Button sx={{ color: green[500], borderColor: green[500], '&:hover': { color: green[500], borderColor: green[500] }, }} variant="outlined" > Secondary </Button> </Stack> ); }

Button Sizes

The size prop of the Button component allows us to create buttons of different sizes.

JavaScript
import { Box, Button } from '@mui/material'; export default function App() { return ( <Box> <Box sx={{ '& button': { m: 1 } }}> <div> <Button size="small">Small</Button> <Button size="medium">Medium</Button> <Button size="large">Large</Button> </div> <div> <Button variant="outlined" size="small" > Small </Button> <Button variant="outlined" size="medium" > Medium </Button> <Button variant="outlined" size="large" > Large </Button> </div> <div> <Button variant="contained" size="small" > Small </Button> <Button variant="contained" size="medium" > Medium </Button> <Button variant="contained" size="large" > Large </Button> </div> </Box> </Box> ); }
Creating buttons of different sizes in Material UI.

Icon and Label Buttons

Including an icon in a button can make clearer to the user the action the button performs. Assigning the icon component to the startIcon or endIcon prop aligns the icon to the left or right of the label respectively.

JavaScript
import { Button, Stack } from '@mui/material'; import { Settings as SettingsIcon, PlayArrow as PlayArrowIcon, } from '@mui/icons-material'; export default function App() { return ( <Stack spacing={2} direction="row" > <Button variant="contained" startIcon={<PlayArrowIcon />} > Play </Button> <Button variant="outlined" endIcon={<SettingsIcon />} > Settings </Button> </Stack> ); }
Creating a button with an icon and a label.

Icon Buttons in Material UI

Icon buttons can help save screen space and ease user recognition. We can use the IconButton component from Material UI to create them.

JavaScript
import { IconButton, Stack } from '@mui/material'; import { Settings, Delete, Info, ContentCopy } from '@mui/icons-material'; export default function App() { return ( <Stack spacing={2} direction="row" > <IconButton> <Settings /> </IconButton> <IconButton color="primary"> <Delete /> </IconButton> <IconButton color="secondary"> <Info /> </IconButton> <IconButton disabled color="primary" > <ContentCopy /> </IconButton> </Stack> ); }
Creating icon buttons in Material UI.

Icon Button Sizes

Like Button, IconButton also comes with a size prop for customizing its size.

JavaScript
<IconButton size="small"> <Settings fontSize="small" /> </IconButton> <IconButton size="medium"> <Settings fontSize="medium" /> </IconButton> <IconButton size="large"> <Settings fontSize="large" /> </IconButton>
Create icon button components of different sizes.

Icon Button Colors

The color prop lets us apply a color from the theme palette to an IconButton.

JavaScript
import { IconButton, Stack } from '@mui/material'; import { Settings as SettingsIcon } from '@mui/icons-material'; export default function App() { return ( <Stack spacing={1} direction="row" > <IconButton color="primary"> <SettingsIcon /> </IconButton> <IconButton color="secondary"> <SettingsIcon /> </IconButton> <IconButton color="success"> <SettingsIcon /> </IconButton> <IconButton color="error"> <SettingsIcon /> </IconButton> <IconButton color="warning"> <SettingsIcon /> </IconButton> </Stack> ); }
Customizing icon button colors.

Loading Buttons in Material UI

A loading button can indicate an ongoing operation and temporarily disable interaction. We can create one with the LoadingButton component.

JavaScript
import { Stack } from '@mui/material'; import { LoadingButton } from '@mui/lab'; import { Save as SaveIcon } from '@mui/icons-material'; export default function App() { return ( <Stack spacing={2} direction="row" > <LoadingButton loading variant="contained" > Play </LoadingButton> <LoadingButton loading loadingIndicator="Loading..." variant="outlined" > Send message </LoadingButton> <LoadingButton loading loadingPosition="start" startIcon={<SaveIcon />} variant="outlined" > Save </LoadingButton> </Stack> ); }
Creating a loading button in Material UI.

How to Use Tabs in Material UI

Tabs let users explore and switch between groups of related content and add an additional layer of navigation to apps.

In this article, we’re going to learn how to easily create tabs in Material UI with the Tabs and Tab components.

The Material UI Tabs Component

We can use the Tabs component from Material UI to create a group of tabs. It has a value prop that sets the currently selected tab using its zero-based index.

The Tab component creates each tab. Its label prop sets the tab title.

import { Box, Tab, Tabs } from '@mui/material';
import { useState } from 'react';

function App() {
  const [tabIndex, setTabIndex] = useState(0);

  const handleTabChange = (event, newTabIndex) => {
    setTabIndex(newTabIndex);
  };

  return (
    <Box>
      <Tabs value={tabIndex} onChange={handleTabChange}>
        <Tab label="Tab 1" />
        <Tab label="Tab 2" />
        <Tab label="Tab 3" />
      </Tabs>
    </Box>
  );
}

export default App;
Creating tabs in Material UI.

We add a listener to the onChange prop to perform an action when the user tries to select another tab. We can access the index of the new tab in the listener. In the above example, we use React state simply set a new Tabs value in the listener.

Changing the currently selected tab in Material UI.

We can display different content for each tab using the Tabs value. For example:

import { Box, Tab, Tabs, Typography } from '@mui/material';
import { useState } from 'react';

function App() {
  const [tabIndex, setTabIndex] = useState(0);

  const handleTabChange = (event, newTabIndex) => {
    setTabIndex(newTabIndex);
  };

  return (
    <Box>
      <Box>
        <Tabs value={tabIndex} onChange={handleTabChange}>
          <Tab label="Tab 1" />
          <Tab label="Tab 2" />
          <Tab label="Tab 3" />
        </Tabs>
      </Box>
      <Box sx={{ padding: 2 }}>
        {tabIndex === 0 && (
          <Box>
            <Typography>The first tab</Typography>
          </Box>
        )}
        {tabIndex === 1 && (
          <Box>
            <Typography>The second tab</Typography>
          </Box>
        )}
        {tabIndex === 2 && (
          <Box>
            <Typography>The third tab</Typography>
          </Box>
        )}
      </Box>
    </Box>
  );
}

export default App;
Displaying different content for each tab.

Material UI Tab Colors

The Tabs component comes with certain props that set the colors of different parts of the tabs. The textColor prop sets the color of each tab title, and the indicatorColor prop sets the indicator color.

<Tabs
  value={tabIndex}
  onChange={handleTabChange}
  textColor="secondary"
  indicatorColor="secondary"
>
  <Tab label="Tab 1" />
  <Tab label="Tab 2" />
  <Tab label="Tab 3" />
</Tabs>
Customizing tab colors in Material UI.

Custom Tab Colors

textColor only allows a value of primary, secondary, or inherit. This means we can only use the color of the Tabs parent component, or one of the primary and secondary theme colors. Similarly, indicatorColor only allows values of primary and secondary. To set custom colors, we can override the styles of certain classes from Material UI:

  • MuiTabs-indicator: styles the tab indicator.
  • MuiTab-root: styles each tab.
  • MuiSelected: styles the currently selected tab.
import { Box, Tab, Tabs } from '@mui/material';
import { blue, red } from '@mui/material/colors';
import { useState } from 'react';

function App() {
  const [tabIndex, setTabIndex] = useState(0);

  const handleTabChange = (event, newTabIndex) => {
    setTabIndex(newTabIndex);
  };

  return (
    <Box>
      <Box>
        <Tabs
          value={tabIndex}
          onChange={handleTabChange}
          sx={{
            '& .MuiTabs-indicator': { backgroundColor: red[500] },
            '& .MuiTab-root': { color: blue[700] },
            '& .Mui-selected': { color: red[500] },
          }}
        >
          <Tab label="Tab 1" />
          <Tab label="Tab 2" />
          <Tab label="Tab 3" />
        </Tabs>
      </Box>
    </Box>
  );
}

export default App;
Using custom colors for the tabs.

Wrapped Tab Labels

We can set the wrapped prop to true for a Tab to make the tab label wrap if it is too long.

<Tabs
  value={tabIndex}
  onChange={handleTabChange}
>
  <Tab
    label="Lorem ipsum, dolor sit amet consectetur adipisicing elit"
    wrapped
  />
  <Tab label="Tab 2" />
  <Tab label="Tab 3" />
</Tabs>
Using a wrapped label for a tab.

Disabled Tabs

Setting the disabled prop to true on a Tab prevents it from being selected.

<Tabs
  value={tabIndex}
  onChange={handleTabChange}
>
  <Tab label="Active" />
  <Tab
    label="Disabled"
    disabled
  />
  <Tab label="Active" />
</Tabs>

Full Width Tabs in Material UI

We can set the fullWidth prop of a Tabs to true to make the tabs occupy the entire width of the viewport. This is useful for smaller views.

<Tabs
  value={tabIndex}
  onChange={handleTabChange}
  variant="fullWidth"
>
  <Tab label="Tab 1" />
  <Tab label="Tab 2" />
  <Tab label="Tab 3" />
</Tabs>
Creating full-width tabs in Material UI.

Centered Tabs

For larger views, we can use the centered prop to position the tabs at the center.

<Tabs
  value={tabIndex}
  onChange={handleTabChange}
  centered
>
  <Tab label="Tab 1" />
  <Tab label="Tab 2" />
  <Tab label="Tab 3" />
</Tabs>
Centering tabs.

Scrollable Tabs in Material UI

The scrollable prop lets the user scroll through the tabs if they overflow their container.

<Tabs
  value={tabIndex}
  onChange={handleTabChange}
  variant="scrollable"
  scrollButtons="auto"
  sx={{ width: 500 }}
>
  <Tab label="Tab 1" />
  <Tab label="Tab 2" />
  <Tab label="Tab 3" />
  <Tab label="Tab 4" />
  <Tab label="Tab 5" />
  <Tab label="Tab 6" />
  <Tab label="Tab 7" />
  <Tab label="Tab 8" />
  <Tab label="Tab 9" />
  <Tab label="Tab 10" />
</Tabs>
Scrollable tabs in Material UI.

The scroll buttons let the user scroll towards the right or left.

Scrolling towards the right and left.

The scrollButtons prop controls the display of the scroll buttons. It can take three possible values:

  1. auto – only displays the scroll buttons when not all the items are visible. Hides them below a certain viewport width.
  2. true – always displays the scroll buttons.
  3. false – never displays the scroll buttons.

In the following example, we set scrollButtons to false to hide the scroll buttons.

<Tabs
  value={tabIndex}
  onChange={handleTabChange}
  variant="scrollable"
  scrollButtons={false}
  sx={{ width: 500 }}
>
  <Tab label="Tab 1" />
  <Tab label="Tab 2" />
  <Tab label="Tab 3" />
  <Tab label="Tab 4" />
  <Tab label="Tab 5" />
  <Tab label="Tab 6" />
  <Tab label="Tab 7" />
  <Tab label="Tab 8" />
  <Tab label="Tab 9" />
  <Tab label="Tab 10" />
</Tabs>
Hiding the scroll buttons.

Vertical Tabs in Material UI

To display vertical tabs, we can set the Tabs orientation prop to vertical. It is horizontal by default.

import { Box, Tab, Tabs, Typography } from '@mui/material';
import { useState } from 'react';

function App() {
  const [tabIndex, setTabIndex] = useState(0);

  const handleTabChange = (event, newTabIndex) => {
    setTabIndex(newTabIndex);
  };

  return (
    <Box>
      <Box sx={{ display: 'flex' }}>
        <Tabs
          value={tabIndex}
          onChange={handleTabChange}

          orientation="vertical"
        >
          <Tab label="Tab 1" />
          <Tab label="Tab 2" />
          <Tab label="Tab 3" />
        </Tabs>
        <Box sx={{ margin: 2 }}>
          {tabIndex === 0 && (
            <Box>
              <Typography>The first tab</Typography>
            </Box>
          )}
          {tabIndex === 1 && (
            <Box>
              <Typography>The second tab</Typography>
            </Box>
          )}
          {tabIndex === 2 && (
            <Box>
              <Typography>The third tab</Typography>
            </Box>
          )}
        </Box>
      </Box>
    </Box>
  );
}

export default App;
Vertical tabs in Material UI.

Icon Tabs

We can the icon prop to display an icon label for a tab instead of text.

<Tabs
  value={tabIndex}
  onChange={handleTabChange}
>
  <Tab icon={<SearchIcon />} />
  <Tab icon={<StarIcon />} />
  <Tab icon={<SettingsIcon />} />
</Tabs>
Tabs with only icons used for the labels.

We could also use it with the label prop to display both icon and text for the label.

<Tabs
  value={tabIndex}
  onChange={handleTabChange}
>
  <Tab
    icon={<SearchIcon />}
    label="Search"
  />
  <Tab
    icon={<StarIcon />}
    label="Favorites"
  />
  <Tab
    icon={<SettingsIcon />}
    label="Settings"
  />
</Tabs>
Tabs with both icons and text displayed for the labels.

Tab Icon Position

The iconPosition prop sets the position of the icon in the tab content. It can be top, bottom, start, or end.

<Tabs
  value={tabIndex}
  onChange={handleTabChange}
>
  <Tab
    icon={<AlarmIcon />}
    label="top"
  />
  <Tab
    icon={<SearchIcon />}
    iconPosition="start"
    label="start"
  />
  <Tab
    icon={<StarIcon />}
    iconPosition="end"
    label="end"
  />
  <Tab
    icon={<FavoriteIcon />}
    iconPosition="bottom"
    label="bottom"
  />
</Tabs>
Tabs with different icon positions set.

Conclusion

Tabs organize groups of related content into separate views where only one view can be visible at a time. We can use the Tabs and Tab components from Material UI to easily create and customize them in our apps.

How to Convert Minutes to Hours and Minutes in JavaScript

To convert minutes to hours and minutes in JavaScript, divide the minutes by 60. The hours will be the whole number of the result, and the minutes will be the remainder of the division.

For example:

function toHoursAndMinutes(totalMinutes) {
  const hours = Math.floor(totalMinutes / 60);
  const minutes = totalMinutes % 60;

  return { hours, minutes };
}

// { hours: 1, minutes: 40 }
console.log(toHoursAndMinutes(100));

// { hours: 1, minutes: 0 }
console.log(toHoursAndMinutes(60));

// { hours: 2, minutes: 10 }
console.log(toHoursAndMinutes(130));

We create a reusable function that takes the total number of minutes and returns an object containing the separate hour and minute values.

First, we divide the total minutes by 60 to get the number of full hours. The division will result in a floating point number, so we use the Math.floor() function to get just the whole number of the quotient.

Math.floor() returns the largest integer less than or equal to a specified number.

console.log(Math.floor(10.95)); // 10
console.log(Math.floor(10)); // 10
console.log(Math.floor(10.05)); // 10

After this, we use the modulo operator (%) to get the remainder of dividing the total minutes by 60. The result is the remaining minutes.

console.log(100 % 60); // 40
console.log(60 % 60); // 0
console.log(130 % 60); // 10

We return an object with hour and minute properties having values of the full hours and remaining minutes respectively.

Convert minutes to time string (HH:mm)

We could also return results in other formats, depending on our use case. For example, we could return the hours and minutes as a time string in a format like HH:mm, where two digits are used for both the hours and the minutes.

function toHoursAndMinutes(totalMinutes) {
  const hours = Math.floor(totalMinutes / 60);
  const minutes = totalMinutes % 60;

  return `${padToTwoDigits(hours)}:${padToTwoDigits(minutes)}`;
}

function padToTwoDigits(num) {
  return num.toString().padStart(2, '0');
}

console.log(toHoursAndMinutes(100)); // 01:40
console.log(toHoursAndMinutes(60)); // 01:00
console.log(toHoursAndMinutes(130)); // 02:10

Here we use the padStart() method to pad the hour and minute values with zeroes if they are single digits.

The String padStart() method pads the current string with another string as many times as needed until the resulting string reaches the specified length.

const str1 = '7';
const str2 = 'abc';

console.log(str1.padStart(3, '0')); // 007
console.log(str2.padStart(5, '*')); // **abc
console.log(str2.padStart(3, '*')); // abc

The string we return is of the time format HH:mm, but you can use another format to suit your use case.

Return string with abbreviated labels

In the following example, we return a string containing the full hour indicated with an h, and the remaining minutes indicated with an m (if it exists):

function toHoursAndMinutes(totalMinutes) {
  const hours = Math.floor(totalMinutes / 60);
  const minutes = totalMinutes % 60;

  return `${hours}h${minutes > 0 ? ` ${minutes}m` : ''}`;
}

console.log(toHoursAndMinutes(100)); // 1h 40m
console.log(toHoursAndMinutes(60)); // 1h
console.log(toHoursAndMinutes(130)); // 2h 10m

We use the ternary operator to ensure that the remaining minutes are more than zero before displaying them with the abbreviation.

How to Convert a String to a Date Object in JavaScript

To convert a string to a Date object, call the Date() constructor, passing the string as an argument, i.e., const date = new Date(str).

JavaScript
const str = '2022-06-15'; const date = new Date(str); console.log(date.getFullYear()); // 2022 console.log(date.getDate()); // 15 console.log(date); // 2022-06-15T00:00:00.000Z

If a string is passed, the Date() constructor creates a Date object from the information in the string.

Invalid dates

If the passed string has a wrong or unsupported format, an error will be thrown or an invalid date will be created, depending on the implementation.

JavaScript
const invalidDate = new Date('a'); console.log(invalidDate); // Invalid Date const invalidDate2 = new Date('15/6/2022'); console.log(invalidDate2); // Invalid Date

How to Convert a String with a non-ISO-8601 Format to a Date object

It’s important to make sure that the string you pass to the Date() constructor conforms to the ISO 8601 format (YYYY-MM-DDTHH:mm:ss.sssZ) for consistent parsing behavior across all browsers. Parsing behavior for other formats depends on the implementation and may not work across all browsers.

To convert a string of a format that is not ISO 8601 and retain consistent parsing behavior, we can get the individual date component values from the string and pass them as numbers to the Date() constructor. For example, we can convert a string with the MM/DD/YY format to a Date object like this:

JavaScript
const str = '06/15/2022'; const [month, day, year] = str.split('/'); console.log(month); // 06 console.log(day); // 15 console.log(year); // 2022 const date = new Date(+year, +month - 1, +day); console.log(date); // May 15, 2022

We split the string with the forward-slash (/) using the String split() method to get the individual values for the month, day, and year. We convert them to numbers with the unary operator (+) and pass them to the Date() constructor.

Note: We subtracted 1 from the month when passing it to the Date() constructor because Date() expects a zero-based month value, i.e. 0 = January, 1 = February, 2 = March, etc.

We can take a similar approach when a time is specified in the string. In the following example, the string we convert has hours, minutes, and seconds specified.

JavaScript
const str = '06-15-2022 09:13:50'; const [dateStr, timeStr] = str.split(' '); console.log(dateStr); // 06-15-2022 console.log(timeStr); // 09:13:50 const [month, day, year] = dateStr.split('-'); const [hours, minutes, seconds] = timeStr.split(':'); console.log(month); // 06 console.log(day); // 15 console.log(year); // 2022 const date = new Date( +year, +month - 1, +day, +hours, +minutes, +seconds ); console.log(date); // 2022-06-15T08:13:50.000Z

First, we had to split the string with a space to use the date and time strings separately.

Unlike in the previous example, this time, the values in the date string were separated with a hyphen (-).

So we used this as the separator when calling split() to obtain the month, day, and year individually.

Similarly, the values in the time string were separated with a colon (:), so this was the separator we used to separate them with the String split() method.

After obtaining each date and time value separately, we converted them to numbers with the unary operator (+) and passed them to the Date() constructor.

How to Convert a Date Object to an ISO 8601 String

If we want to convert a Date object to a string for storage in a file or database, we can store it in the ISO 8601 format, so that we can retrieve and easily convert it back to a Date object with browser-independent parsing behavior without using any third-party libraries.

We can do this with the toISOString() method.

JavaScript
const str = '06-15-2022 09:13:50'; const [dateStr, timeStr] = str.split(' '); console.log(dateStr); // 06-15-2022 console.log(timeStr); // 09:13:50 const [month, day, year] = dateStr.split('-'); const [hours, minutes, seconds] = timeStr.split(':'); console.log(month); // 06 console.log(day); // 15 console.log(year); // 2022 const date = new Date( +year, +month - 1, +day, +hours, +minutes, +seconds ); const isoString = date.toISOString(); console.log(isoString); // 2022-06-15T08:13:50.000Z // Can convert back to Date object with browser-independent parsing const sameDate = new Date(isoString); console.log(sameDate.getDate()); // 15 console.log(sameDate.getMinutes()); // 13

The Date toISOString() method returns a string of the date in the ISO 8601 format according to universal time.

The date-fns parse() Function

We can also use parse() function from the date-fns NPM package to easily convert strings of a wide variety of formats to a Date object. Here we specify that the string is of the MM-dd-yyyy hh:m:ss format so that it can be converted correctly.

JavaScript
import { parse } from 'date-fns'; const str = '06-15-2022 09:13:50'; const date = parse(str, 'MM-dd-yyyy hh:m:ss', new Date()); console.log(date.getHours()); // 9 console.log(date.getDate()); // 15 console.log(date); // 2022-06-15T08:13:50.000Z

Key takeaways

  • To convert a string to a Date object in JavaScript, call the Date() constructor and pass the string as an argument.
  • Invalid date formats will result in an error being thrown or an invalid date being created.
  • For non-ISO-8601 date formats, we can split the string and pass the date components as numbers to the Date() constructor.
  • Use the toISOString() method to convert a Date object to an ISO-8601 string for consistent parsing behavior.
  • The date-fns NPM package provides a parse() function to easily convert strings of various formats to a Date object.

Quick user authentication with React + Node.js + Firebase: A complete Guide

Authentication is critical for verifying the identity of your users in order to know what data they should have access to and what privileged actions they should be able to perform. The Firebase platform provides powerful libraries that let us easily integrate authentication into our projects.

In this article, we are going to implement authentication by building a RESTful API and a web app that allows a user to sign up with a secure note that will be accessible only to the user. We’ll be using Node.js and Express to build the API, and React.js to create the single-page web app.

The complete source code for the app is available here on GitHub.

What You’ll Need

  • Node.js installed
  • A Google account – to use Firebase
  • Basic knowledge of React.js and Node.js
  • A code editor – like Visual Studio Code

Setting up Firebase

Before we start coding, let’s head over to the Firebase console and create a new project, so that we can access Firebase services. I’m naming mine cb-auth-tutorial, but you can name yours whatever you like.

Setting up a Firebase project.
Creating a new Firebase project

After giving it a name, you’ll be asked whether you want to enable Google Analytics. We won’t be using the service for this tutorial, but you can turn it on if you like.

After completing all the steps, you’ll be taken to the dashboard, where you can see an overview of your Firebase project. It should look something like this:

The Firebase project dashboard.
The Firebase dashboard

Let’s create a web app. Click this icon button to get started:

Icon button to create a new web app.

You’ll be asked to enter a nickname for the app. This can also be anything you like. I’m naming mine CB Auth Tutorial, for symmetry with the project name.

Create a new web app with Firebase.
Completing the steps to create the web app

After registering the app, you’ll be provided with a configuration that you’ll need to initialize your app with to be able to access the various Firebase APIs and services.

From the dashboard sidebar, click on Build > Authentication, then click on Get started on the screen that shows to enable Firebase Authentication. You’ll be asked to add an initial sign-in method.

The screen in the Firebase console to add the first sign-in method.
Adding a sign-in method

Click on Email/Password and turn on the switch to enable it.

Enabling the "Email/Password" sign-in method.
Enabling sign-in with email/password

Next, we’ll set up Firebase Firestore.

Click on Build > Firestore Database in the sidebar, then click the Create database button on the page that shows to enable Firestore.

You’ll be presented with a dialog that will take you through the steps to create the database.

The dialog to create the Firestore database.
The dialog used to create the Firestore database

We won’t be accessing Firestore from the client-side, so we can create the database in production mode. Firebase Admin ignores security rules when interacting with Firestore.

Next, we’ll need to generate a service account key, which is a JSON file containing information we’ll initialize our admin app with to be able to create the custom web tokens that we’ll send to the client. Follow these instructions in the Firebase Documentation to do this.

Let’s install the Firebase CLI tools with NPM. Run the following command in a terminal to do so:

npm i -g firebase-tools

Let’s create a new folder for the project. I’m naming mine auth-tutorial for symmetry with the Firebase project, but you can name it whatever you like.

Initialize Firebase in the project directory with the following command:

firebase init

We’ll be using Firebase Functions and Firebase Emulators, so select these when asked to choose the features you want to set up for the project directory.

Setting up Firebase features for the project directory.
Setting up Firebase features in the project directory

The next prompt is for you to associate the project directory with a Firebase project. Select Use an existing project and choose the project you created earlier.

Associating the project directory with a Firebase project.
Associating the project directory with a Firebase project

We’ll be using plain JavaScript to write the functions, so choose that when asked about the language you want to use.

We’ll be using the Firebase Functions emulator to test our functions, so select it when asked to set up the emulators.

Setting up Firebase emulators.
Setting up Firebase emulators

After you’ve initialized Firebase, your project directory structure should look like this:

Our project directory structure after initializing Firebase

Creating the REST API

We’ll need the following NPM packages to write our function:

  • express: Node.js web framework to speed up development.
  • cors: Express middleware to enable CORS (Cross-Origin Resource Sharing).
  • morgan: Logger middleware for Express.
  • is-email: For server-side email validation.
  • firebase: To authenticate users with the Firebase Web SDK.

Let’s install them all with one command:

npm i express cors morgan is-email firebase

Let’s write the handler function for the /register endpoint. Create a new folder named express in the functions directory, containing a sub-folder named routes, and create a new register.js file in routes with the following code:

functions/express/routes/register.js

const {
  getAuth,
  createUserWithEmailAndPassword,
} = require('firebase/auth');
const {
  getAuth: getAdminAuth,
} = require('firebase-admin/auth');
const firestore = require('firebase-admin').firestore();

async function register(req, res) {
  const { email, password, secureNote } = req.body;
  if (!secureNote) {
    res
      .status(400)
      .json({ error: { code: 'no-secure-note' } });
    return;
  }

  try {
    const auth = getAuth();
    const credential = await createUserWithEmailAndPassword(
      auth,
      email,
      password
    );
    const adminAuth = getAdminAuth();
    const token = await adminAuth.createCustomToken(
      credential.user.uid
    );
    await firestore
      .doc(`users/${credential.user.uid}`)
      .set({ secureNote });
    res.status(201).json({ token });
  } catch (err) {
    const { code } = err;
    if (code === 'auth/email-already-in-use') {
      res.status(400);
    } else {
      res.status(500);
    }
    res.json({
      error: {
        code: code ? code.replace('auth/', '') : undefined,
      },
    });
  }
}

module.exports = register;

If all validation is successful, the secure note of the new user will be saved in the Firestore database. Let’s create the function that will handle POST requests to the /login endpoint in a new login.js file, also saved in the routes directory.

functions/express/routes/login.js

const {
  getAuth: getClientAuth,
  signInWithEmailAndPassword,
} = require('firebase/auth');
const {
  getAuth: getAdminAuth,
} = require('firebase-admin/auth');

async function login(req, res) {
  const { email, password } = req.body;
  try {
    const credential = await signInWithEmailAndPassword(
      getClientAuth(),
      email,
      password
    );
    const token = await getAdminAuth().createCustomToken(
      credential.user.uid
    );
    res.status(200).json({ token });
  } catch (error) {
    if (
      error.code === 'auth/wrong-password' ||
      error.code === 'auth/user-not-found'
    ) {
      res.status(403);
    } else {
      res.status(500);
    }
    res.json({
      error: { code: error.code.replace('auth/', '') },
    });
  }
}

module.exports = login;

Notice that the /login and /register route handlers don’t perform validation on the email or password sent in a request. This is because we’ll be creating custom Express middleware to do this instead. Create a new middleware sub-folder in the express folder, and create a new validate-email-and-password.js file in it, containing the following code:

functions/express/middleware/validate-email-and-password.js

const isEmail = require('is-email');

function validateEmailAndPassword(req, res, next) {
  const { email, password } = req.body;

  if (!email) {
    res.status(400).send({ error: { code: 'no-email' } });
    return;
  }

  if (!isEmail(email)) {
    res
      .status(400)
      .send({ error: { code: 'invalid-email' } });
    return;
  }

  if (!password) {
    res
      .status(400)
      .send({ error: { code: 'no-password' } });
    return;
  }

  next();
}

module.exports = validateEmailAndPassword;

Here we check that a password and a valid email are specified in the request body. If they are, the request is passed on to the next middleware. Otherwise, we end the request with an error.

Let’s create the endpoint that will allow the fetching of the secure note of a logged-in user. We’ll do this in a new get-user.js file saved in the routes folder.

functions/express/routes/get-user.js

const firestore = require('firebase-admin').firestore();

async function getUser(req, res) {
  const userId = req.params.id;
  if (!userId) {
    res.status(400).json({ error: { code: 'no-user-id' } });
    return;
  }

  if (userId !== req.token.uid) {
    res
      .status(403)
      .json({ error: { code: 'unauthorized' } });
  }

  const snapshot = await firestore
    .collection('users')
    .doc(userId)
    .get();
  if (!snapshot.exists) {
    res
      .status(404)
      .json({ error: { code: 'user-not-found' } });
    return;
  }
  const user = snapshot.data();

  res.status(200).json({ secureNote: user.secureNote });
}

module.exports = getUser;

We respond with an error if a user is not specified, or the user making the request for the data is not the owner.

req.token.uid is supplied by another middleware that verifies the token sent along when making an authenticated request to the API. Let’s create this middleware in a firebase-auth.js file located in the express/middleware folder.

functions/express/middleware/firebase-auth.js

const { getAuth } = require('firebase-admin/auth');

async function firebaseAuth(req, res, next) {
  const regex = /Bearer (.+)/i;
  try {
    const idToken =
      req.headers['authorization'].match(regex)?.[1];
    req.token = await getAuth().verifyIdToken(idToken);
    next();
  } catch (err) {
    res
      .status(401)
      .json({ error: { code: 'unauthenticated' } });
  }
}

module.exports = firebaseAuth;

We verify that the JSON web token sent is a valid token and assign it to the req.token property if so. Otherwise, we send a 401 error.

Now it’s time to integrate all these modules together in an Express app that will respond to any request made to the api cloud function. Replace the index.js file in the functions folder with the following code:

const functions = require('firebase-functions');
const express = require('express');
const admin = require('firebase-admin');
const validateEmailAndPassword = require('./express/middleware/validate-email-and-password');
const firebaseConfig = require('./firebase.config');
const { initializeApp } = require('firebase/app');
const cors = require('cors');
const morgan = require('morgan');
const serviceAccount = require('./service-account-key.json');

admin.initializeApp({
  credential: admin.credential.cert(serviceAccount),
});
initializeApp(firebaseConfig);

const register = require('./express/routes/register');
const login = require('./express/routes/login');
const firebaseAuth = require('./express/middleware/firebase-auth');
const getUser = require('./express/routes/get-user');

const app = express();
app.use(cors());
app.use(morgan('dev'));

app.post('/login', validateEmailAndPassword, login);
app.post('/register', validateEmailAndPassword, register);
app.get('/users/:id', firebaseAuth, getUser);

exports.api = functions.https.onRequest(app);

This file will be run to start Firebase Functions. We used the initializeApp() method from the firebase-admin module to initialize the Firebase Admin SDK with the service account key file you should have created earlier.

We also used the initalizeApp() method from the firebase/app module to initialize Firebase Web with a configuration stored in a firebase.config.js file. You were given this configuration earlier when you created the web app in the Firebase console.

functions/firebase.config.js

/**
  Enter the configuration for your Firebase web app
  module.exports = {
  apiKey: ...,
  authDomain: ...,
  projectId: ...,
  storageBucket: ...,
  messagingSenderId: ...,
  appId: ...,
  measurementId: ...
}; */

We can now start Firebase Functions in the emulator, by running the following command in the project directory.

firebase emulators:start --only functions

Testing the API

We haven’t written client code yet but we can test our API with a tool like Postman, or we can use one of the methods described here in the Firebase documentation.

Here we’re test the /register endpoint with Postman:

Testing the /register API endpoint with Postman.
Testing the /register endpoint with Postman

Creating the Client App with React

Let’s write the client app that will interact with our RESTful API. Create a new React app with Create React App.

npx create-react-app client

We’ll be using the following NPM packages in the React app:

  • Material UI (@mui/material, @emotion/react, @emotion/styled): To style our client UI and make it attractive.
  • axios: To make HTTP requests to the API we’ve created.
  • react-router-dom: For single-page app routing.
  • react-hook-form: For easier React form validation.
  • firebase: The Firebase Web SDK library.
  • react-firebase-hooks: Provides a set of reusable React hooks for Firebase.
  • is-email: For client-side email validation.
npm install @mui/material @emotion/react @emotion/styled axios react-router-dom react-hook-form firebase react-firebase-hooks is-email

To finish setting up Material UI, we’ll add the Roboto font by placing this link element within the head tag in our client/public/index.html file.

<link
  rel="stylesheet"
  href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"
/>

Start React in the client directory with:

npm start

Test that the app is up and running by opening localhost:3000 in your browser. You’ll see the results of the standard React.js boilerplate in your client/src/App.js file. We’ll edit this file later.

Testing the newly created React app.
Testing the newly created React app

The URL origin of the cloud functions running in an emulator is different from the one it has when running in a production environment. Let’s create a .env file to specify the different origins. The values you’ll need to specify will depend on the name you gave your Firebase project.

client/src/.env

REACT_APP_CF_PROD_=https://us-central1-cb-auth-tutorial.cloudfunctions.net
REACT_APP_CF_DEV=http://localhost:5001/cb-auth-tutorial/us-central1

We’ll also create a functions-origin.js module that will provide the correct origin depending on our current Node environment.

client/src/functions-origin.js

export const CLOUD_FUNCTIONS_ORIGIN =
  process.env.NODE_ENV === 'development'
    ? process.env.REACT_APP_CF_DEV
    : process.env.REACT_APP_CF_PROD;

Let’s create a module that would be responsible for making the HTTP requests to our RESTful API using axios. Create this module in an api-service.js file.

Here’s the code for the module:

client/src/api-service.js

import axios from 'axios';
import { CLOUD_FUNCTIONS_ORIGIN } from './functions-origin';

const apiUrl = `${CLOUD_FUNCTIONS_ORIGIN}/api`;

export async function signIn({ email, password }) {
  const url = `${apiUrl}/login`;
  const res = await axios.post(url, { email, password });
  return res.data;
}

export async function signUp({
  email,
  password,
  secureNote,
}) {
  const url = `${apiUrl}/register`;
  const res = await axios.post(url, {
    email,
    password,
    secureNote,
  });
  return res.data;
}

export async function getUserData({ userIdToken, userId }) {
  const url = `${apiUrl}/users/${userId}`;
  const res = await axios.get(url, {
    headers: {
      Authorization: `Bearer ${userIdToken}`,
    },
  });
  return res.data;
}

After this, we’ll need to create a few utilities to help with authentication. Create a new auth.js file with the following code:

client/src/auth.js

import * as apiService from './api-service';
import { useLocation, Navigate } from 'react-router-dom';
import {
  useEffect,
  createContext,
  useContext,
} from 'react';
import {
  getAuth,
  signInWithCustomToken,
  signOut as firebaseSignOut,
} from 'firebase/auth';
import { useAuthState } from 'react-firebase-hooks/auth';

export function RequireAuth({ children }) {
  let auth = useAuth();
  let location = useLocation();

  useEffect(() => {}, [auth.loading]);

  return auth.loading ? undefined : auth.user ? (
    children
  ) : (
    <Navigate
      to="/signin"
      state={{ from: location }}
      replace
    />
  );
}

export const AuthContext = createContext(undefined);

export function useAuth() {
  return useContext(AuthContext);
}

export function AuthProvider({ children }) {
  const auth = getAuth();
  const [user, loading] = useAuthState(auth);

  const signIn = async ({ email, password }) => {
    const { token } = await apiService.signIn({
      email,
      password,
    });
    await signInWithCustomToken(auth, token);
  };

  const signUp = async ({
    email,
    password,
    secureNote,
  }) => {
    const { token } = await apiService.signUp({
      email,
      password,
      secureNote,
    });
    await signInWithCustomToken(getAuth(), token);
  };

  const signOut = async () => {
    const auth = getAuth();
    await firebaseSignOut(auth);
  };

  const value = { user, loading, signIn, signOut, signUp };

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

Wrapping a route component in the RequireAuth component will ensure that only authenticated users will be able to view it. If not signed in, the user will be taken to the /signin route and then redirected back to the route that they trying to view after a successful sign-in.

The AuthProvider component allows its children to access important authentication-related data and methods using a React context and its provider. The useAuth() hook will provide the context values to the child components with the useContext() hook.

The signIn() and signUp() methods make requests to the API. If successful, a token will be received and passed the signInWithCustomToken() method from the firebase/auth module to authenticate the user in the browser.

Now it’s time to create the sign-up page. Users sign up with an email, a password, and a secure note. We’ll do this in a SignUp.jsx file in a new routes folder.

client/src/routes/SignUp.jsx

import {
  Box,
  Button,
  LinearProgress,
  TextField,
  Typography,
} from '@mui/material';
import { useState } from 'react';
import { useForm, Controller } from 'react-hook-form';

import isEmail from 'is-email';
import { useAuth } from '../auth';
import { useNavigate } from 'react-router-dom';

export default function SignUp() {
  const {
    control,
    handleSubmit,
    setError,
    formState: { errors },
  } = useForm();
  const [errorMessage, setErrorMessage] =
    useState(undefined);
  const [isSigningUp, setIsSigningUp] = useState(false);
  const { signUp } = useAuth();
  const navigate = useNavigate();

  const onSubmit = async (data) => {
    const { email, password, secureNote } = data;
    setIsSigningUp(true);
    setErrorMessage(undefined);
    try {
      await signUp({ email, password, secureNote });
      navigate('/');
    } catch (error) {
      const res = error.response;
      if (res) {
        const code = res.data?.error?.code;
        if (code === 'email-already-in-use') {
          setError('email', {
            message: 'This email is taken',
          });
          return;
        }
      }
      setErrorMessage("Can't sign up right now");
    } finally {
      setIsSigningUp(false);
    }
  };

  return (
    <Box
      sx={{
        height: '100%',
        display: 'flex',
        justifyContent: 'center',
        alignItems: 'center',
      }}
    >
      <form
        onSubmit={handleSubmit(onSubmit)}
        style={{ display: 'flex', flexDirection: 'column' }}
      >
        <Controller
          control={control}
          name="email"
          rules={{
            required: 'Enter an email',
            validate: {
              validateEmail: (email) =>
                isEmail(email) || 'Enter a valid email',
            },
          }}
          render={({ field }) => (
            <TextField
              {...field}
              label="Email"
              helperText={errors?.email?.message}
              error={Boolean(errors.email)}
              type="email"
            />
          )}
        />
        <Controller
          control={control}
          name="password"
          rules={{ required: 'Enter a password' }}
          render={({ field }) => (
            <TextField
              label="Password"
              {...field}
              helperText={errors?.password?.message}
              error={Boolean(errors.password)}
              sx={{ marginTop: 2 }}
              type="password"
            />
          )}
        />
        <Controller
          control={control}
          name="secureNote"
          rules={{ required: 'Enter a secure note' }}
          render={({ field }) => (
            <TextField
              {...field}
              label="Secure note"
              helperText={errors?.secureNote?.message}
              error={Boolean(errors?.secureNote)}
              sx={{ marginTop: 2 }}
            />
          )}
        />
        <LinearProgress
          variant="indeterminate"
          sx={{
            marginTop: 2,
            visibility: isSigningUp ? 'visible' : 'hidden',
          }}
        />
        <Button
          variant="contained"
          type="submit"
          sx={{ marginTop: 2 }}
        >
          Sign up
        </Button>
        <Box sx={{ marginTop: 2, textAlign: 'center' }}>
          <Typography
            sx={{
              visibility: errorMessage
                ? 'visible'
                : 'hidden',
            }}
            color="error"
          >
            {errorMessage}
          </Typography>
        </Box>
      </form>
    </Box>
  );
}

We use the Controller component from react-hook-form to register the Material UI TextField component with react-hook-form. We set validation rules with the Controller rules prop to ensure that the user enters a valid email, a password, and a secure note.

Form validation on the sign-up page.
Form validation on the sign-up page

react-hook-form ensures that the onSubmit() function is only called when all the validation rules have been satisfied. In this function, we register the user with the signUp() method from the useAuth() hook we created earlier. If successful, we take the user to the index route (/). Otherwise, we display the appropriate error message.

Displaying an error message in the sign-up page.
Displaying an error message after receiving an API response

Let’s also create the sign-in page in a SignIn.jsx file in the same routes folder.

client/src/routes/SignIn.jsx

import {
  Box,
  Button,
  LinearProgress,
  TextField,
  Typography,
} from '@mui/material';
import { useState } from 'react';
import { useForm, Controller } from 'react-hook-form';
import isEmail from 'is-email';
import { useNavigate } from 'react-router-dom';
import { useAuth } from '../auth';

export default function SignIn() {
  const {
    control,
    handleSubmit,
    setError,
    formState: { errors },
  } = useForm();
  const [errorMessage, setErrorMessage] =
    useState(undefined);
  const navigate = useNavigate();
  const { signIn } = useAuth();

  const onSubmit = async (data) => {
    const { email, password } = data;
    setIsSigningIn(true);
    setErrorMessage(undefined);
    try {
      await signIn({ email, password });
      navigate('/');
    } catch (error) {
      const res = error.response;
      if (res) {
        const code = res.data?.error?.code;
        if (code === 'user-not-found') {
          setError('email', {
            message: 'No user has this email',
          });
          return;
        }
        if (code === 'wrong-password') {
          setError('password', {
            message: 'Wrong password',
          });
          return;
        }
      }
      setErrorMessage("Can't sign in right now");
    } finally {
      setIsSigningIn(false);
    }
  };

  const [isSigningIn, setIsSigningIn] = useState(false);

  return (
    <Box
      sx={{
        display: 'flex',
        justifyContent: 'center',
        alignItems: 'center',
        height: '100%',
      }}
    >
      <form
        onSubmit={handleSubmit(onSubmit)}
        style={{
          display: 'flex',
          flexDirection: 'column',
        }}
      >
        <Controller
          control={control}
          name="email"
          rules={{
            required: 'Enter an email',
            validate: {
              validateEmail: (email) =>
                isEmail(email) || 'Enter a valid email',
            },
          }}
          render={({ field }) => (
            <TextField
              label="Email"
              {...field}
              helperText={errors.email?.message}
              error={Boolean(errors.email)}
              type="email"
            />
          )}
        />
        <Controller
          control={control}
          name="password"
          rules={{ required: 'Enter a password' }}
          render={({ field }) => (
            <TextField
              label="Password"
              {...field}
              helperText={errors.password?.message}
              error={Boolean(errors.password)}
              sx={{ marginTop: 2 }}
              type="password"
            />
          )}
        />
        <LinearProgress
          variant="indeterminate"
          sx={{
            visibility: isSigningIn ? 'visible' : 'hidden',
            marginTop: 2,
          }}
        />
        <Button
          variant="contained"
          type="submit"
          sx={{ marginTop: 2 }}
        >
          Sign in
        </Button>
        <Box
          sx={{
            marginTop: 2,
            textAlign: 'center',
          }}
        >
          <Typography
            sx={{
              visibility: errorMessage
                ? 'visible'
                : 'hidden',
            }}
            color="error"
          >
            {errorMessage}
          </Typography>
        </Box>
      </form>
    </Box>
  );
}

Unlike in the SignUp component, here we use the signIn() method from the useAuth() hook to sign the user in.

The HTTP errors we handle here are different from the ones we handle in SignUp. In SignUp, we display an error if the email the user attempted to sign up with has already been used. But here we display errors for a non-existent email or a wrong password.

An error message is displayed for a wrong password.
Displaying an error message for a wrong password after receiving an API response

Now let’s create the component that will be shown for our index route. Replace the contents of App.js with the following:

client/src/App.js

import logo from './logo.svg';
import './App.css';
import { useAuth } from './auth';
import { useEffect, useRef, useState } from 'react';
import { Button, Typography, Box } from '@mui/material';
import { Link } from 'react-router-dom';
import * as apiService from './api-service';

function App() {
  const { user, loading } = useAuth();
  const [dataState, setDataState] = useState(undefined);
  const secureNoteRef = useRef(undefined);

  useEffect(() => {
    (async () => {
      if (!loading) {
        if (user) {
          setDataState('loading');
          const userIdToken = await user.getIdToken();
          try {
            const { secureNote } =
              await apiService.getUserData({
                userIdToken,
                userId: user.uid,
              });
            secureNoteRef.current = secureNote;
            setDataState('success');
          } catch {
            setDataState('error');
          }
        }
      }
    })();
  }, [user, loading]);

  const child = loading ? (
    <></>
  ) : user ? (
    dataState === 'loading' ? (
      <Typography>Getting your data...</Typography>
    ) : dataState === 'error' ? (
      <Typography>An error occured.</Typography>
    ) : dataState === 'success' ? (
      <div>
        <Typography variant="h6">Secure note</Typography>
        <Typography>{secureNoteRef.current}</Typography>
      </div>
    ) : undefined
  ) : (
    <div>
      <Typography>You're not signed in</Typography>
      <Box
        sx={{
          marginTop: 2,
        }}
      >
        <Button LinkComponent={Link} to="/signin">
          Sign in
        </Button>
        <Button
          LinkComponent={Link}
          to="/signup"
          sx={{ marginLeft: 2 }}
        >
          Sign up
        </Button>
      </Box>
    </div>
  );
  return (
    <div
      style={{
        display: 'flex',
        justifyContent: 'center',
        alignItems: 'center',
        height: '100%',
      }}
    >
      {child}
    </div>
  );
}

export default App;

If the user hasn’t been authenticated, we let them know they’re not signed in and include the relevant links to do so.

The view shown to a user that is yet to be authenticated.
The view displayed to a user that is yet to be authenticated

If they’ve signed in, we make a request to the API to get the secure note and display it.

Displaying the secure note to the user.
Displaying the private secure note to the user

We used a dataState variable to keep track of the current state of the API request and display an appropriate view to the user based on this.

We set dataState to loading just before making the request to let the user know that their data is in the process of being retrieved.

The view shown when "dataState" is "loading".
The view displayed when dataState is loading.

If an error occurs in this process, we let them know by setting dataState to error:

The view displayed when dataState is error.

Finally, let’s initialize Firebase and set up the routing logic in our index.js file.

client/src/index.js

import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import {
  BrowserRouter,
  Route,
  Routes,
} from 'react-router-dom';
import SignIn from './routes/SignIn';
import { AuthProvider } from './auth';
import { initializeApp } from 'firebase/app';
import firebaseConfig from './firebase.config';
import SignUp from './routes/SignUp';

initializeApp(firebaseConfig);

const root = ReactDOM.createRoot(
  document.getElementById('root')
);
root.render(
  <React.StrictMode>
    <AuthProvider>
      <BrowserRouter>
        <Routes>
          <Route path="/" element={<App />} />
          <Route path="/signin" element={<SignIn />} />
          <Route path="/signup" element={<SignUp />} />
        </Routes>
      </BrowserRouter>
    </AuthProvider>
  </React.StrictMode>
);

reportWebVitals();

There should be a firebase.config.js file in your src directory that contains the config you received when setting up the web app in the Firebase console. This is the same config we used to initialize the Web SDK in the Admin environment when we were writing the API.

client/src/firebase.config.js

/**
  Enter the configuration for your Firebase web app
  module.exports = {
  apiKey: ...,
  authDomain: ...,
  projectId: ...,
  storageBucket: ...,
  messagingSenderId: ...,
  appId: ...,
  measurementId: ...
}; */

The app should be fully functional now!

Conclusion

In this article, we learned how to easily set up authentication in our web apps using Firebase. We created a RESTful API with Node.js and the Express framework to handle requests from a client app that we built using React.js and Material UI.

How to Remove an Element from an Array by ID in JavaScript

Sometimes we might be working with an array of objects with unique IDs that allow each of them to be uniquely identified among other objects.

For example:

const arr = [
  { id: 1, name: 'John' },
  { id: 2, name: 'Peter' },
  { id: 3, name: 'Kate' },
];

What if we want to remove an object that has a particular ID from the array? We’re going to learn multiple ways of doing so in this article.

1. Array findIndex() and splice() methods

To remove an element from an array by ID in JavaScript, use the findIndex() method to find the index of the object with the ID in the array. Then call the splice(index, 1) method on the array to remove the object from the array.

function removeObjectWithId(arr, id) {
  const objWithIdIndex = arr.findIndex((obj) => obj.id === id);

  if (objWithIdIndex > -1) {
    arr.splice(objWithIdIndex, 1);
  }

  return arr;
}

const arr = [
  { id: 1, name: 'John' },
  { id: 2, name: 'Kate' },
  { id: 3, name: 'Peter' },
];

removeObjectWithId(arr, 2);

// [ { id: 1, name: 'John' }, { id: 3, name: 'Peter' } ]
console.log(arr);

The Array findIndex() method returns the index of the first element in an array that passes a test specified by a callback function.

const arr = [9, 8, 7, 6, 5];

const index = arr.findIndex((num) => num === 7);
console.log(index); // 2

We specify a test that an object in that array will pass only if it has an ID equal to the specified ID. This makes findIndex() return the index of the object with that ID.

The Array splice() method changes the contents of an array by removing existing elements while adding new ones simultaneously.

const arr1 = ['a', 'b', 'c'];

// Removing elements
arr1.splice(1, 2);
console.log(arr1); // [ 'a' ]

// Removing and adding new elements
const arr2 = ['a', 'b', 'c'];
arr2.splice(1, 2, 'd', 'e');
console.log(arr2); // [ 'a', 'd', 'e' ]

This method takes three arguments:

  1. start – the index at which to start changing the array.
  2. deleteCount – the number of elements to remove from start
  3. item1, item2, ... – a variable number of elements to add to the array, beginning from start.

We specify a deleteCount of 1 and a start of the target index to make splice() remove just the object with that index from the array. We didn’t specify any more arguments, so nothing is added to the array.

If no object passes the specified test, findIndex() will return -1. Because of this, we add an if check to ensure that the index is 0 or greater.

Without this if check, splice() will be called with -1 as its first argument, and will remove the last element of the array, when there’s actually no element with the ID in the array:

const arr = [
  { id: 1, name: 'John' },
  { id: 2, name: 'Kate' },
  { id: 3, name: 'Peter' },
];

// No such element with ID of 9
const objWithIdIndex = arr.findIndex((obj) => obj.id === 9);

console.log(objWithIdIndex); // -1

// ❌ No if check

arr.splice(objWithIdIndex, 1); // ❌ Removes last element!

// [ { id: 1, name: 'John' }, { id: 2, name: 'Kate' } ]
console.log(arr);

Avoid side effects

The Array splice() method mutates the passed array. This introduces a side effect into our removeObjectWithId() function. To avoid modifying the passed array and create a pure function, make a copy of the array and call splice() on the copy, instead of the original

function removeObjectWithId(arr, id) {
  // Making a copy with the Array from() method
  const arrCopy = Array.from(arr);

  const objWithIdIndex = arrCopy.findIndex((obj) => obj.id === id);
  arrCopy.splice(objWithIdIndex, 1);
  return arrCopy;
}

const arr = [
  { id: 1, name: 'John' },
  { id: 2, name: 'Kate' },
  { id: 3, name: 'Peter' },
];

const newArr = removeObjectWithId(arr, 2);

// [ { id: 1, name: 'John' }, { id: 3, name: 'Peter' } ]
console.log(newArr);

// original not modified
/**
[
  { id: 1, name: 'John' },
  { id: 2, name: 'Kate' },
  { id: 3, name: 'Peter' }
]
 */
console.log(arr);

Tip

Functions that don’t modify external state (i.e., pure functions) tend to be more predictable and easier to reason about. This makes it a good practice to limit the number of side effects in your programs.

2. Array filter() method

We can also remove an element from an array by ID with the filter() method. We call filter() on the array, passing a callback that returns true for every element in that array apart from the object with the specified ID.

Example

function removeObjectWithId(arr, id) {
  return arr.filter((obj) => obj.id !== id);
}

const arr = [
  { id: 1, name: 'John' },
  { id: 2, name: 'Kate' },
  { id: 3, name: 'Peter' },
];

const newArr = removeObjectWithId(arr, 2);

// [ { id: 1, name: 'John' }, { id: 3, name: 'Peter' } ]
console.log(newArr);

// original not modified
/**
[
  { id: 1, name: 'John' },
  { id: 2, name: 'Kate' },
  { id: 3, name: 'Peter' }
]
 */
console.log(arr);

The Array filter() method creates a new array filled with elements that pass a test specified by a callback function. It does not modify the original array.

Example

const arr = [1, 2, 3, 4];

const filtered = arr.filter((num) => num > 2);
console.log(filtered); // [ 3, 4 ]

In our example, we set a test that an object in the array will pass only if its id property is not equal to the specified ID. This ensures that the object with the specified ID is not included in the new array returned from filter().

How to Remove the ID Attribute From an Element With JavaScript

To remove the ID attribute from an element, call the removeAttribute() method on the element, passing the string 'id' as an argument.

Example

index.html

<!DOCTYPE html>
<html>
  <head>
    <title>
      Removing the ID Attribute from an Element with
      JavaScript
    </title>
  </head>
  <body>
    <div class="box" id="box-1">This is a box</div>

    <script src="index.js"></script>
  </body>
</html>

Here’s how we can remove the ID from the div element we created:

index.js

const box = document.getElementById('box-1');

console.log(box.getAttribute('id')); // box-1

box.removeAttribute('id');

console.log(box.getAttribute('id')); // null

The Element removeAttribute() method removes the attribute of an element with a specified name.

Note: If the specified attribute doesn’t exist, removeAttribute() returns instead of throwing an error.

Element setAttribute() method

If you would like to replace the ID of the element instead of removing it, you can use the setAttribute() method.

For example, we can change the ID of the div element we created earlier to a new value.

For example:

index.js

const box = document.getElementById('box-1');

console.log(box.getAttribute('id')); // box-1

box.setAttribute('id', 'new-id');

console.log(box.getAttribute('id')); // new-id

The Element setAttribute() method takes two arguments:

  1. name: A string specifying the name of the attribute whose value is to be set.
  2. value: A string containing the value to assign to the attribute.

We pass 'id' and the new id as the first and second arguments respectively, to set the ID attribute.

Note: If the attribute already exists on the element, the value is updated. Otherwise, a new attribute is added with the specified name and value.

The Element getAttribute() method

We’ve used the getAttribute() method a number of times in this article. Calling this method on an Element returns the value of the attribute with the specified name. So we call it with 'id' to get the ID of the element.

Note: If the attribute does not exist on the element, getAttribute() returns null or an empty string (''), depending on the browser.

How to Add One Day to a Date in JavaScript

1. Date setDate() and getDate() Methods

To add one day to a Date in JavaScript, call the getDate() method on the Date to get the day of the month, then call the setDate() method on the Date, passing the sum of the result of getDate() and 1 as an argument.

For example:

function addOneDay(date) {
  date.setDate(date.getDate() + 1);
  return date;
}

const date = new Date('2022-05-15T00:00:00.000Z');

const newDate = addOneDay(date);

// 2022-05-16T00:00:00.000Z
console.log(newDate);

Our addOneDay() function takes a Date object and the number of days to add as arguments, and returns the same Date object with the day incremented by one.

The Date getDate() method returns a number between 1 and 31 that represents the day of the month of the particular date.

The Date setDate() method changes the day of the month of the Date object to the number passed as an argument.

Note

If the number you specify would change the month or year of the DatesetDate() automatically updates the Date information to reflect this.

// April 25, 2022
const date = new Date('2022-04-25T00:00:00.000Z');

date.setDate(40);

// May 10, 2022
console.log(date); // 2022-05-10T00:00:00.000Z

console.log(date.getDate()); // 10

May has only 30 days, so passing 40 to setDate() increments the month by one and sets the day of the month to 10.

Avoiding Side Effects

The setDate() method mutates the Date object it is called on. This introduces a side effect into our addOneDay() function. To avoid modifying the passed Date and create a pure function, make a copy of the Date and call setDate() on this copy, instead of the original.

function addOneDay(date) {
  const dateCopy = new Date(date);
  dateCopy.setDate(date.getDate() + 1);
  return dateCopy;
}

const date = new Date('2022-05-15T00:00:00.000Z');

const newDate = addOneDay(date);

console.log(newDate); // 2022-05-16T00:00:00.000Z

// original not modified
console.log(date); // 2022-05-15T00:00:00.000Z

Tip

Functions that don’t modify external state (i.e., pure functions) tend to be more predictable and easier to reason about. This makes it a good practice to limit the number of side effects in your programs.

2. date-fns addDays() function

Alternatively, you can use the pure addDays() function from the date-fns NPM package to quickly add one day to a Date. This function takes a Date object and the number of days to add as arguments, and returns a new Date object with the newly added days.

import { addDays } from 'date-fns';

const date = new Date('2022-05-15T00:00:00.000Z');

const newDate = addDays(date, 1);

console.log(newDate); // 2022-05-16T00:00:00.000Z

// original not modified
console.log(date); // 2022-05-15T00:00:00.000Z

How to Add Days to a Date in JavaScript

1. Date setDate() and getDate() Methods

To add days to a Date in JavaScript, call the getDate() method on the Date to get the day of the month, then call the setDate() method on the Date, passing the sum of getDate() and the number of days to add.

For example:

function addDays(date, days) {
  date.setDate(date.getDate() + days);
  return date;
}

const date = new Date('2022-05-15T00:00:00.000Z');

const newDate = addDays(date, 5);

// 2022-05-20T00:00:00.000Z
console.log(newDate);

Our addDays() function takes a Date object and the number of days to add as arguments, and returns the same Date object with the newly added days.

The Date getDate() method returns a number between 1 and 31 that represents the day of the month of the particular date.

The Date setDate() method changes the day of the month of the Date object to the number passed as an argument.

If the number you specify would change the month or year of the Date, setDate() automatically updates the Date information to reflect this.

// April 25, 2022
const date = new Date('2022-04-25T00:00:00.000Z');

date.setDate(40);

// May 10, 2022
console.log(date); // 2022-05-10T00:00:00.000Z

console.log(date.getDate()); // 10

April has only 30 days, so passing 40 to setDate() increments the month by one and sets the day of the month to 10.

Avoiding Side Effects

The setDate() method mutates the Date object it is called on. This introduces a side effect into our addDays() function. To avoid modifying the passed Date and create a pure function, make a copy of the Date and call setDate() on this copy, instead of the original.

function addDays(date, days) {
  const dateCopy = new Date(date);
  dateCopy.setDate(date.getDate() + days);
  return dateCopy;
}

const date = new Date('2022-05-15T00:00:00.000Z');

const newDate = addDays(date, 5);

console.log(newDate); // 2022-05-20T00:00:00.000Z

// original not modified
console.log(date); // 2022-05-15T00:00:00.000Z

Functions that don’t modify external state (i.e., pure functions) tend to be more predictable and easier to reason about. This makes it a good practice to limit the number of side effects in your programs.

2. date-fns addDays() Function

Alternatively, you can use the pure addDays() function from the date-fns NPM package to quickly add days to a Date.

import { addDays } from 'date-fns';

const date = new Date('2022-05-15T00:00:00.000Z');

const newDate = addDays(date, 5);

console.log(newDate); // 2022-05-20T00:00:00.000Z

// original not modified
console.log(date); // 2022-05-15T00:00:00.000Z

How to Check if a String Contains Only Letters and Spaces in JavaScript

1. The RegExp test() Method

To check if a string contains only letters and spaces in JavaScript, call the test() method on this regex: /^[A-Za-z\s]*$/. If the string contains only letters and spaces, this method returns true. Otherwise, it returns false.

function onlyLettersAndSpaces(str) {
  return /^[A-Za-z\s]*$/.test(str);
}

const str1 = 'contains_underscore';
const str2 = 'only letters and spaces';

console.log(onlyLettersAndSpaces(str1)); // false
console.log(onlyLettersAndSpaces(str2)); // true

The RegExp test() method searches for a match between the regular expression and a specified string.

The / and / characters are used to start and end the regular expression.

The ^ character matches the beginning of the string, while the $ character matches the end of the string.

The square brackets ([]) are used to match any one of multiple specified patterns. In our example, we specify three patterns: A-Za-z, and \s. A-Z matches any uppercase letter, a-z matches any lowercase letter, and 0-9 matches any digit.

The * character matches zero or more occurrences of a particular pattern. We add it after the square brackets to match any of the patterns in the brackets as many times as possible.

How to Check if a String Contains At Least One Letter and One Space

The regular expression we used makes the method return true if the string contains only letters or only spaces.

const str1 = 'OnlyLetters';
const str2 = '  '; // only spaces
const str3 = 'letters and spaces';

console.log(onlyLettersAndSpaces(str1)); // true
console.log(onlyLettersAndSpaces(str2)); // true
console.log(onlyLettersAndSpaces(str3)); // true

To make sure the string contains at least one letter and one space, we’ll need to match the string against a regex that matches at least one letter (/[A-Za-z]/), and another that matches at least one space /\s/.

function atLeastOneLetterAndSpace(str) {
  return (
    /^[A-Za-z\s]*$/.test(str) &&
    /[A-Za-z]/.test(str) &&
    /\s/.test(str)
  );
}

const str1 = 'OnlyLetters';
const str2 = '  '; // Only spaces
const str3 = 'letters and spaces';

console.log(atLeastOneLetterAndSpace(str1)); // false
console.log(atLeastOneLetterAndSpace(str2)); // false
console.log(atLeastOneLetterAndSpace(str3)); // true

2. The String match() Method

We can also use the String match() method to check if a string contains only letters and spaces.

function onlyLettersAndSpaces(str) {
  return Boolean(str?.match(/^[A-Za-z\s]*$/));
}

const str1 = 'contains_underscore';
const str2 = 'only letters and spaces';

console.log(onlyLettersAndSpaces(str1)); // false
console.log(onlyLettersAndSpaces(str2)); // true

The String match() method returns an array of all the matches of a regular expression in a string. If there are no matches, it returns null.

const regex = /^[A-Za-z\s]*$/;

const str1 = 'contains_underscore';
const str2 = 'only letters and spaces';

// null
console.log(str1?.match(regex));

/**
[
  'only letters and spaces',
  index: 0,
  input: 'only letters and spaces',
  groups: undefined
]
 */
console.log(str2?.match(regex));

We pass the result of match() to the Boolean constructor to convert it to a BooleanBoolean() converts truthy values to true, and falsy values to false.

In JavaScript, there are six falsy values: undefinednullNaN0'' (empty string), and false. Every other value is truthy.

console.log(Boolean(undefined)); // false
console.log(Boolean(['letters'])); // true
console.log(Boolean(null)); // false
console.log(Boolean(5)); // true

We used the optional chaining operator (?.) on the string variable. If the variable is nullish (undefined or null), instead of an error being thrown when we try to call the match() method on it, this operator will prevent the method call and return undefined.

const str = null;

console.log(str?.match(/^[A-Za-z\s]*$/)); // undefined