featured

How to Create an Animated Countdown Timer with Vue

Timer apps are everywhere, and they each have their unique appearance and design. Some opt for a minimalistic design, using only text to indicate the time left, while others try to be more visual by displaying a slowly decreasing pie shape or even playing audio at regular intervals to notify of the time remaining.

Well here’s the sort of timer we’ll be building in this article:

The animated timer we'll be building for the article.

It indicates the time left with length, text, and color, which makes it very demonstrative.

We’ll be using Vue, so set up your project and let’s get started!

Creating the Timer Ring

We’ll start by creating the timer ring using an svg element. It will contain a circle element that we’ll use to create the timer ring. The circle will be drawn at the center of the svg container, with a radius of 45 pixels.

src/components/AppTimer.vue

<template>
  <div class="root">
    <svg class="svg" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
      <g class="circle">
        <circle class="time-elapsed-path" cx="50" cy="50" r="45" />
      </g>
    </svg>
  </div>
</template>

We’ll wrap the circle element in a g element so that we’ll be able to group the circle with other elements that’ll be added later in the tutorial.

We’ve created the basic HTML markup, now let’s add CSS that will display the ring.

src/components/AppTimer.vue

...
<style>
/* Sets the container's height and width */
.root {
  height: 300px;
  width: 300px;
  position: relative;
}

/* Removes SVG styling that would hide the time label */
.circle {
  fill: none;
  stroke: none;
}

/* The SVG path that displays the timer's progress */
.time-elapsed-path {
  stroke-width: 7px;
  stroke: #424242;
}
</style>

Let’s register the AppTimer component in App.vue and display it:

src/App.vue

<template>
  <AppTimer />
</template>

<script>
import AppTimer from './components/AppTimer.vue';

export default {
  name: 'App',
  components: {
    AppTimer,
  },
};
</script>

So this is what we have right now. Just a basic ring. Let’s keep moving.

A circle ring.

Displaying the Timer Label

The next thing to do after creating the timer ring is to show the label that indicates how much time is left.

src/components/AppTimer.vue

<template>
  <div class="root">
    ...
    <div class="time-left-container">
      <span class="time-left-label">{{ timeLeftString }}</span>
    </div>
  </div>
</template>

<script>
export default {
  methods: {
    padToTwo(num) {
      // e.g. 4 -> '04'
      return String(num).padStart(2, '0');
    },
  },
  computed: {
    // e.g. timeLeft of 100 -> '01:40'
    timeLeftString() {
      const timeLeft = this.timeLeft;
      const minutes = Math.floor(timeLeft / 60);
      const seconds = timeLeft % 60;
      return `${this.padToTwo(minutes)}:${this.padToTwo(seconds)}`;
    },
    timeLeft() {
      return this.limit - this.elapsed;
    },
  },
  // Register props to be set from App.vue
  props: {
    elapsed: {
      type: Number,
      required: true,
    },
    limit: {
      type: Number,
      required: true,
    },
  },
};
</script>
...

The AppTimer now has two props. The elapsed prop will be used to set how much time has elapsed, and the limit prop will specify the total time.

timeLeft() is a computed property that will be automatically updated when elapsed changes.

timeLeftString() is another computed property that will return a string in the MM:SS format indicating the timer left. Its values will be updated whenever timeLeft() changes.

Let’s add the following CSS to AppTimer.vue, which will style the label and overlay it on top of the timer ring:

src/components/AppTimer.vue

...
<style>
...
.time-left-container {
  /* Size should be the same as that of parent container */
  height: inherit;
  width: inherit;

  /* Place container on top of circle ring */
  position: absolute;
  top: 0;

  /* Center content (label) vertically and horizontally  */
  display: flex;
  align-items: center;
  justify-content: center;
}

.time-left-label {
  font-size: 70px;
  font-family: 'Segoe UI';
  color: black;
}
</style>

Let’s set the AppTimer props we created, from App.vue:

src/App.vue

<template>
  <AppTimer :elapsed="0" :limit="10" />
</template>
...

Now we can see the label.

The timer now has a label.

Enabling Timer Countdown

A timer has no use if it can’t count down, let’s add some logic to enable this functionality.

We’ll use a state variable (timeElapsed) to keep track of the total time elapsed so far in seconds. Using the setInterval() method, we will increment this variable by 1 every 1000 milliseconds (1 second). We’ll also ensure that we stop the regular increments once all the timer has elapsed, using the clearInterval() method.

All this logic will be contained in the startTimer() method. We’ll call startTimer() in the mounted() hook, so that the countdown starts immediately after the page loads.

src/App.vue

<template>
  <AppTimer
    :elapsed="timeElapsed"
    :limit="timeLimit"
  />
</template>

<script>
import AppTimer from './components/AppTimer.vue';

export default {
  name: 'App',
  data() {
    return {
      timeElapsed: 0,
      timerInterval: undefined,
      timeLimit: 10,
    };
  },
  methods: {
    startTimer() {
      this.timerInterval = setInterval(() => {
        // Stop counting when there is no more time left
        if (++this.timeElapsed === this.timeLimit) {
          clearInterval(this.timerInterval);
        }
      }, 1000);
    },
  },
  // Start timer immediately
  mounted() {
    this.startTimer();
  },
  components: {
    AppTimer,
  },
};
</script>

And now we have a functional timer.

Creating the Timer Progress Ring

Now we need to add a ring that will be animated to visualize the time remaining. We’ll give this ring a distinctive color and place it on the gray ring. As time passes it will animate to reveal more and more of the gray ring until only the gray ring is visible when no time is remaining.

We’ll create the ring using a path element and style it with CSS:

src/components/AppTimer.vue

<template>
  <div class="root">
    <svg
      class="svg"
      viewBox="0 0 100 100"
      xmlns="http://www.w3.org/2000/svg"
    >
      <g class="circle">
        ...
        <path
          class="time-left-path"
          d="
            M 50, 50
            m -45, 0
            a 45,45 0 1,0 90,0
            a 45,45 0 1,0 -90,0
          "
        ></path>
      </g>
    </svg>
    ...
  </div>
</template>

<script>
...
</script>

<style>
...
.time-left-path {
  /* Same thickness as the original ring */
  stroke-width: 7px;

  /* Rounds the path endings  */
  stroke-linecap: round;

  /* Makes sure the animation starts at the top of the circle */
  transform: rotate(90deg);
  transform-origin: center;

  /* One second aligns with the speed of the countdown timer */
  transition: 1s linear all;

  /* Colors the ring */
  stroke: blue;
}

.svg {
  /* Flips the svg and makes the animation to move left-to-right */
  transform: scaleX(-1);
}
</style>

So now this blue ring covers the gray ring.

The blue timer ring now convers the gray ring.

Animating the Timer Progress Ring

To animate the ring, we are going to use the stroke-dasharray attribute of the path.

Here’s how the ring will look with different stroke-dasharray values:

How the blue timer ring will look with different stroke-dasharray values.

We can see that setting stroke-dasharray to a single value creates a pattern of dashes (the blue arcs) and gaps (spaces between the dashes) that have the same length. stroke-dasharray adds as many dashes as possible to fill up the entire length of the path.

As the name suggests, stroke-dasharray can also take multiple values. Let’s see what happens when we specify two:

Specify two values for stroke-dasharray.
stroke-dasharray: 10 30

When two values are specified, the first value will determine the length of the dashes, and the second value will determine the length of the gaps.

We can use this behavior to make the blue path visualize the time left. To do this, first, let us calculate the total length of the circle made by the blue path using the circle circumference formula (2Ï€r):

Full path length = 2 x π x r = 2 x π x 45 = 282.6 ≈ 283

So to display the time left with the path, we’ll specify two values, the first value will start at 283 and gradually reduce to 0, while the second value will be constant at 283. This ensures that there is only one dash and one gap at all times, since 283 is as long as the entire path.

Here’s how the path will change in length as the first value changes:

How the blue path length changes as the first stroke-dasharray value changes.

Let’s implement this in our code:

src/components/AppTimer.vue

<template>
  <div class="root">
    <svg
      class="svg"
      viewBox="0 0 100 100"
      xmlns="http://www.w3.org/2000/svg"
    >
      <g class="circle">
        ...
        <path
          class="time-left-path"
          v-if="timeLeft > 0"
          d="
            M 50, 50
            m -45, 0
            a 45,45 0 1,0 90,0
            a 45,45 0 1,0 -90,0
          "
          :style="{ strokeDasharray }"
        ></path>
      </g>
    </svg>
    ...
  </div>
</template>

<script>
export default {
  ...
  computed: {
    ...
    strokeDasharray() {
      const radius = 45;
      const totalLength = 2 * Math.PI * radius;
      const timeFraction = this.timeLeft / this.limit;
      const elapsedDash = Math.floor(timeFraction * totalLength);
      return `${elapsedDash} ${totalLength}`;
    },
  },
  ...
};
</script>

<style>
...
</style>

We use a computed property (strokeDasharray) to calculate the new value for the stroke-dasharray attribute whenever the time left changes.

We use v-if to stop showing the blue ring entirely when there is no more time remaining.

And now the blue ring animates in line with the label to indicate the time left.

The blue ring animates in line with the label now.

There’s one issue though: if you watch closely, the blue ring suddenly disappears when the timer reaches zero.

The blue path suddenly disappears at this length as the timer reaches zero.

This happens because the animation duration is set to one second. When the value of the remaining time is set to zero, it still takes one second to actually animate the blue ring to zero.

To fix this, we can use a formula that will reduce the length of a ring by an additional amount (separate from the animation) every time one second passes. Let’s modify AppTimer.vue to do this:

src/AppTimer.vue

<template>
...
</template>

<script>
export default {
  ...
  computed: {
    ...
    strokeDasharray() {
      const radius = 45;
      const total = 2 * Math.PI * radius;
      const timeFraction = this.timeLeft / this.limit;
      const adjTimeFraction = timeFraction - (1 - timeFraction) / this.limit;
      const elapsedDash = Math.floor(adjTimeFraction * total);
      return `${elapsedDash} ${total}`;
    },
  },
  ...
};
</script>

<style>
...
</style>

Now the blue ring is reduced to the end before being removed by v-if:

The blue ring is reduced to the end before being removed by v-if.

Creating the Background

We’re done with the timer now, so we need to work on the background. We’ll create and style it in App.vue:

src/App.vue

<template>
  <div class="background">
    <div
      class="elapsed"
      :style="{ height: '100%', backgroundColor: 'blue' }"
    ></div>
  </div>
  <AppTimer
    :elapsed="timeElapsed"
    :limit="timeLimit"
  />
</template>

<script>
...
</script>

<style scoped>
.background {
  height: 100%;
  position: absolute;
  top: 0;
  width: 100%;
  display: flex;
  flex-direction: column;
  justify-content: end;
  background-color: black;
}

.background .elapsed {
  /* For the height animation */
  transition: all 1s linear;
}
</style>

<style>
html,
body,
#app {
  height: 100%;
  margin: 0;
}

#app {

  /* Center timer vertically and horizontally */
  display: flex;
  justify-content: center;
  align-items: center;

  position: relative;
}
</style>

Since we set the background color to blue, we’ll need to change the color of the timer ring and the timer label for them to still be visible.

We’ll use white:

src/components/AppTimer.vue

...
<style>
...
.time-left-label {
  ...
  color: white;
}

.time-left-path {
  ...
  /* Colors the ring */
  stroke: white;
}
...
</style>

We can now see the background:

We can now see the background.

Animating the Background

The background is nice, but it’s just static. Let’s add some code to animate its height as time passes.

src/App.vue

<template>
  <div class="background">
    <div
      class="elapsed"
      :style="{ height: backgroundHeight, backgroundColor: 'blue' }"
      v-if="timeLeft > 0"
    ></div>
  </div>
  <AppTimer
    :elapsed="timeElapsed"
    :limit="timeLimit"
  />
</template>

<script>
...
export default {
  ...
  computed: {
    timeLeft() {
      return this.timeLimit - this.timeElapsed;
    },
    timeFraction() {
      return this.timeLeft / this.timeLimit;
    },
    backgroundHeight() {
      const timeFraction = this.timeFraction;

      // Adjust time fraction to prevent lag when the time left
      // is 0, like we did for the time left progress ring
      const adjTimeFraction =
        timeFraction - (1 - timeFraction) / this.timeLimit;

      const height = Math.floor(adjTimeFraction * 100);

      return `${height}%`;
    },
  },
  ...
};
</script>
...

And now we have a background animation, which serves as another indicator of the time left.

Animating the background height.

Changing the Background Color at Certain Points in Time

It would be great if we could also use color to indicate the time left.

We’ll define certain points in time at which the background color will change, using a thresholds array. The background color will be blue at the start of the countdown. It will change to orange at 50% percent of the total time, and red at 20% of the total time.

src/components/App.vue

...
<script>
...
export default {
  ...
  data() {
    return {
      ...
      thresholds: [
        {
          color: 'blue',
          threshold: 1,
        },
        {
          color: 'orange',
          threshold: 0.5,
        },
        {
          color: 'red',
          threshold: 0.2,
        },
      ],
    };
  },
  ...
};
</script>
...

We’ll use the reduce() method to get the color for the current time from the thresholds array.

src/components/App.vue

<template>
  <div class="background">
    <div
      class="elapsed"
      :style="{ height: backgroundHeight, backgroundColor }"
      v-if="timeLeft > 0"
    ></div>
  </div>
  ...
</template>

<script>
...
export default {
  ...
  computed: {
    ...
    backgroundColor() {
      return this.thresholds.reduce(
        (color, item) =>
          item.threshold >= this.timeFraction ? item.color : color,
        undefined
      );
    },
  },
  ...
};
</script>
...

And we’re done! We have a functional timer, that counts down and displays how much time is left in 4 different ways.

Explore the Source Code for This App

You can view the complete source code of this mini-app here on GitHub.

How to Build an Advanced Space Remover Tool With React

In this article, we’re going to learn how to build a web app that will let us easily remove leading and trailing spaces from any text, with the ability to optionally preserve the indentation of the text. We’ll be using the React.js library to build this tool, let’s get started.

Setting up the Project

Let’s begin by creating a new React app using Create React App. We’ll be using Yarn.

yarn create-react-app remove-spaces

We’ll also be using a bit of TypeScript, you can set it up using the instructions here.

Writing the removeSpaces() Function

The core part of the app will be a removeSpaces() function that takes a string as input and returns a new string with the spaces removed. Let’s write this function in a new remove-spaces.ts file.

src/remove-spaces.ts

export default function removeSpaces(params: {
  text: string;
  leading: boolean;
  trailing: boolean;
  preserveIndent: boolean;
}) {
  let regex: RegExp;
  const { text, leading, trailing, preserveIndent } = params;
  let spaceCountPattern: string | undefined;

  let leadingMatch: string;
  if (leading) {
    if (preserveIndent) {
      const firstSpacePattern = new RegExp(String.raw`^(\s*).+?((\r\n)|\n|$)`);
      const firstSpaces = text.match(firstSpacePattern)?.[1];
      const spaceCount = firstSpaces?.length;
      spaceCountPattern = `{0,${spaceCount}}`;
    } else {
      spaceCountPattern = '*';
    }
    leadingMatch = String.raw`\s${spaceCountPattern}`;
  } else {
    leadingMatch = '';
  }

  const trailingMatch = trailing ? String.raw`\s*?` : '';
  regex = new RegExp(String.raw`((()((\r\n)|\n))|(.*?((\r\n)|\n|$)))`, 'g');
  const lines = text.match(regex);
  const lineRegex = new RegExp(
    String.raw`^${leadingMatch}(.*?)${trailingMatch}((\r\n)|\n|$)`,
    'g'
  );

  const result = lines
    ?.map((line) => {
      if (line === '\r\n' || line === '\n') return line;
      return line.replace(lineRegex, '$1$2');
    })
    .join('');
  return result;
}

Apart from the input string, the function accepts options that will allow the user to customize how the spaces are removed.

When leading is true and preserveIndent is false, the leading spaces are removed from the text, apart from the spaces that add indentation.

When leading is true and preserveIndent is false, all the leading spaces are removed from the text.

When trailing is true, all the trailing spaces are removed from the text.

The function creates a regular expression from the combination of these options. It uses the String replace() method to replace each line of the text with captured groups from the regex.

Testing the removeSpaces() function

We can test this function to be sure it works as intended. Let’s install the Jest testing framework to do this.

yarn add --dev jest ts-jest @types/jest

Initialize ts-jest with the following command:

yarn ts-jest config:init

Let’s write some tests for the function in a new remove-spaces.test.ts file:

src/remove-spaces.test.ts

import removeSpaces from './remove-spaces';

const s2 = '  ';
const s4 = '    ';

const text = `${s4}<div>${s4}
${s4}${s2}<p></p>${s4}
${s4}</div>${s4}`;

it('removes leading spaces without preserving indent', () => {
  const expectation = `<div>${s4}
<p></p>${s4}
</div>${s4}`;
  const result = removeSpaces({
    text,
    leading: true,
    trailing: false,
    preserveIndent: false,
  });
  expect(result).toBe(expectation);
});

it('removes leading spaces and preserves indent', () => {
  const expectation = `<div>${s4}
${s2}<p></p>${s4}
</div>${s4}`;
  const result = removeSpaces({
    text,
    leading: true,
    trailing: false,
    preserveIndent: true,
  });
  expect(result).toBe(expectation);
});

it('removes trailing spaces', () => {
  const expectation = `${s4}<div>
${s4}${s2}<p></p>
${s4}</div>`;
  const result = removeSpaces({
    text,
    leading: false,
    trailing: true,
    preserveIndent: false,
  });
  expect(result).toBe(expectation);
});

it('removes leading and trailing spaces', () => {
  const expectation = `<div>
<p></p>
</div>`;
  const result = removeSpaces({
    text,
    leading: true,
    preserveIndent: false,
    trailing: true,
  });
  expect(result).toBe(expectation);
});

The function should pass all these tests if it was written correctly.

Creating the Text Inputs

It’s time for us to start creating the user interface with React. We’ll begin with the text inputs. We’ll create two – one will take will user input, and the other will be readonly and display the output.

We’ll be using the Material UI framework to make the app look great, you can set it up using the instructions here.

src/App.js

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

function App() {
  const [input, setInput] = useState('');
  const [output, setOutput] = useState('');

  const handleInputChange = (event) => {
    setInput(event.target.value);
  };

  return (
    <Box
      sx={{
        height: '100%',
        display: 'flex',
        flexDirection: 'column',
        padding: 2,
        boxSizing: 'border-box',
      }}
    >
      <Box
        sx={{
          display: 'grid',
          gridTemplateColumns: 'repeat(auto-fit, minmax(400px, 1fr))',
          justifyContent: 'stretch',
          marginTop: 2,
          rowGap: '16px',
        }}
      >
        <Box sx={{ flex: 1, marginRight: 1, textAlign: 'left' }}>
          <Typography>Input</Typography>
          <TextField
            sx={{ width: '100%', marginTop: 1, minWidth: '300px' }}
            multiline
            value={input}
            minRows={10}
            inputProps={{
              style: { maxHeight: '300px', overflow: 'auto' },
            }}
            onChange={handleInputChange}
          ></TextField>
        </Box>
        <Box sx={{ flex: 1, marginLeft: 1, textAlign: 'right' }}>
          <Typography>Output</Typography>
          <TextField
            sx={{
              width: '100%',
              marginTop: 1,
              minWidth: '300px',
            }}
            multiline
            value={output}
            readOnly
            minRows={10}
            inputProps={{
              style: { maxHeight: '300px', overflow: 'auto' },
            }}
          ></TextField>
        </Box>
      </Box>
    </Box>
  );
}

export default App;
Creating the text inputs.

Pasting Input from the Clipboard

Let’s create a button that will paste text from the system clipboard to the input text field when clicked.

src/App.js

// ...
import { Box, Typography, TextField, Stack, Button } from '@mui/material';
import { ContentPaste } from '@mui/icons-material';

function App() {
  // ...

  const pasteInput = async () => {
    setInput(await navigator.clipboard.readText());
  };

  const handlePasteInput = async () => {
    await pasteInput();
  };

  return (
    <Box
      sx={{
        height: '100%',
        display: 'flex',
        flexDirection: 'column',
        padding: 2,
        boxSizing: 'border-box',
      }}
    >
      <Stack
        direction="row"
        spacing={2}
        justifyContent="center"
        sx={{ flexWrap: 'wrap', marginTop: 2 }}
      >
        <Box>
          <Button
            onClick={handlePasteInput}
            variant="outlined"
            startIcon={<ContentPaste />}
          >
            Paste input
          </Button>
        </Box>
      </Stack>
      {/* ... */}
    </Box>
  );
}

export default App;
Pasting input from the clipboard.

Adding Options

Let’s create the options that will let the user decide how the spaces will be removed from the text. There will be three boolean options, each represented with a checkbox:

  1. Remove leading spaces
  2. Remove trailing spaces
  3. Preserve indent

We’ll pass the options directly to the removeSpaces() function when the user decides to remove the spaces.

import {
  Box,
  Typography,
  TextField,
  Stack,
  Button,
  FormControlLabel,
  Checkbox,
} from '@mui/material';
import { useState } from 'react';
import { ContentPaste } from '@mui/icons-material';

function App() {
  // ...

  const [leading, setLeading] = useState(true);
  const [trailing, setTrailing] = useState(true);
  const [preserveIndent, setPreserveIndent] = useState(true);

  const handleLeadingChange = (event) => {
    setLeading(event.target.checked);
  };

  const handleTrailingChange = (event) => {
    setTrailing(event.target.checked);
  };

  const handlePreserveIndentChange = (event) => {
    setPreserveIndent(event.target.checked);
  };

  return (
    <Box
      sx={{
        height: '100%',
        display: 'flex',
        flexDirection: 'column',
        padding: 2,
        boxSizing: 'border-box',
      }}
    >
      <Box sx={{ display: 'flex', justifyContent: 'center' }}>
        <FormControlLabel
          control={
            <Checkbox checked={leading} onChange={handleLeadingChange} />
          }
          label="Remove leading spaces"
        />
        <FormControlLabel
          control={
            <Checkbox
              checked={preserveIndent}
              onChange={handlePreserveIndentChange}
            />
          }
          label="Preserve indent"
        />
        <FormControlLabel
          control={
            <Checkbox checked={trailing} onChange={handleTrailingChange} />
          }
          label="Remove trailing spaces"
        />
      </Box>
     {/* ... */}
    </Box>
  );
}

export default App;
Adding options.

Removing the Spaces

Now let’s add a button that will cause the spaces to be removed from the input text when clicked.


// ...
import removeSpaces from './remove-spaces';

function App() {
  const handleRemoveSpaces = () => {
    setOutput(removeSpaces({ text: input, leading, trailing, preserveIndent }));
  };

  return (
    <Box
      sx={{
        height: '100%',
        display: 'flex',
        flexDirection: 'column',
        padding: 2,
        boxSizing: 'border-box',
      }}
    >
      <Box sx={{ display: 'flex', justifyContent: 'center' }}>
        {/* ... */}
        
        <Box>
          <Button
            onClick={handlePasteInput}
            variant="outlined"
            startIcon={<ContentPaste />}
          >
            Paste input
          </Button>
        </Box>

        {/* Button to remove spaces */}
        <Box>
          <Button
            onClick={handleRemoveSpaces}
            variant="outlined"
            startIcon={<RemoveCircle />}
          >
            Remove spaces
          </Button>
        </Box>
      </Stack>
      {/* ... */}
      </Box>
    </Box>
  );
}

export default App;
Removing the spaces.

Copying Output to Clipboard

Let’s create another button that will copy the text in the output text field to the system clipboard when clicked.


// ...
import { ContentCopy, ContentPaste, RemoveCircle } from '@mui/icons-material';
import removeSpaces from './remove-spaces';

function App() {
  // ...

  const handleCopyOutput = () => {
    navigator.clipboard.writeText(output);
  };

  return (
    <Box
      sx={{
        height: '100%',
        display: 'flex',
        flexDirection: 'column',
        padding: 2,
        boxSizing: 'border-box',
      }}
    >
      {/* ... */}
      <Stack
        direction="row"
        spacing={2}
        justifyContent="center"
        sx={{ flexWrap: 'wrap', marginTop: 2 }}
      >
        {/* ... */}
        <Box>
          <Button
            onClick={handleRemoveSpaces}
            variant="outlined"
            startIcon={<RemoveCircle />}
          >
            Remove spaces
          </Button>
        </Box>

        {/* Button to copy output */}        
        <Box>
          <Button
            startIcon={<ContentCopy />}
            onClick={handleCopyOutput}
            variant="outlined"
          >
            Copy output
          </Button>
        </Box>
      </Stack>
          
    </Box>
  );
}

export default App;
Copying output to clipboard.

Combining Paste, Remove, and Copy Actions

It’s quite likely that users will use this tool by performing the following actions in order:

  1. Click the Paste Input button to put the text from the clipboard in the input text field
  2. Click the Remove Spaces button to remove the spaces from the input text and put the result in the output text field
  3. Click the Copy Output to copy the text from the output text field to the clipboard.

To make things easier, we’ll create a button that will let the user perform these three actions at once:

// ...

function App() {
  // ...

  const handlePasteRemoveCopy = async () => {
    const input = await navigator.clipboard.readText();
    const output = removeSpaces({
      text: input,
      leading,
      trailing,
      preserveIndent,
    });
    navigator.clipboard.writeText(output);
    setInput(input);
    setOutput(output);
  };

  return (
    <Box
      sx={{
        height: '100%',
        display: 'flex',
        flexDirection: 'column',
        padding: 2,
        boxSizing: 'border-box',
      }}
    >
      <Box sx={{ display: 'flex', justifyContent: 'center' }}>
        {/* ... */}
        <FormControlLabel
          control={
            <Checkbox checked={trailing} onChange={handleTrailingChange} />
          }
          label="Remove trailing spaces"
        />
      </Box>

      {/* Button to perform past, remove, and copy actions at once */}
      <Box sx={{ display: 'flex', justifyContent: 'center', marginTop: 2 }}>
        <Button onClick={handlePasteRemoveCopy} variant="contained">
          Paste + Remove + Copy
        </Button>
      </Box>

      <Stack
        direction="row"
        spacing={2}
        justifyContent="center"
        sx={{ flexWrap: 'wrap', marginTop: 2 }}
      >
        <Box>
          <Button
            onClick={handlePasteInput}
            variant="outlined"
            startIcon={<ContentPaste />}
          >
            Paste input
          </Button>
        </Box>
        {/* ... */}
      </Stack>
    </Box>
  );
}

export default App;

Our space remover app is complete! We’ve been able to build a handy utility for removing leading and trailing spaces from any text and preserving indentation if necessary.

What Can This Tool Be Used for?

At Coding Beauty, we found this tool useful when creating code snippets displaying a portion of code from an HTML or JSX markup that was indented by some amount. For example, in our Material UI button tutorial, there were times when the file for an example contained markup like this:

The complete source code for an example in the tutorial.
The complete source code for an example in the tutorial.

But we would only want to show the section of the file relevant to the example:

Explaining contained buttons in the Material UI button tutorial.
Explaining contained buttons in the Material UI button tutorial.

This tool helped format the relevant section properly by removing the spaces.

What about String trim()?

We couldn’t use the trim() or trimStart() string methods because then it wouldn’t be possible to preserve the indent of the entire text. These methods can only remove all the leading spaces in a given string.

11 Amazing New JavaScript Features in ES13 (ES2022)

Like a lot of other programming languages, JavaScript is constantly evolving. Every year, the language is made more powerful with new capabilities that let developers write more expressive and concise code.

Let’s explore the most recent features added in ECMAScript 2022 (ES13), and see examples of their usage to understand them better.

1. Class Field Declarations

Before ES13, class fields could only be declared in the constructor. Unlike in many other languages, we could not declare or define them in the outermost scope of the class.

class Car {
  constructor() {
    this.color = 'blue';
    this.age = 2;
  }
}

const car = new Car();
console.log(car.color); // blue
console.log(car.age); // 2

ES2022 removes this limitation. Now we can write code like this:

class Car {
  color = 'blue';
  age = 2;
}

const car = new Car();
console.log(car.color); // blue
console.log(car.age); // 2

2. Private Methods and Fields

Previously, it was not possible to declare private members in a class. A member was traditionally prefixed with an underscore (_) to indicate that it was meant to be private, but it could still be accessed and modified from outside the class.

class Person {
  _firstName = 'Joseph';
  _lastName = 'Stevens';

  get name() {
    return `${this._firstName} ${this._lastName}`;
  }
}

const person = new Person();
console.log(person.name); // Joseph Stevens

// Members intended to be private can still be accessed
// from outside the class
console.log(person._firstName); // Joseph
console.log(person._lastName); // Stevens

// They can also be modified
person._firstName = 'Robert';
person._lastName = 'Becker';

console.log(person.name); // Robert Becker

With ES13, we can now add private fields and members to a class, by prefixing it with a hashtag (#). Trying to access them from outside the class will cause an error:

class Person {
  #firstName = 'Joseph';
  #lastName = 'Stevens';

  get name() {
    return `${this.#firstName} ${this.#lastName}`;
  }
}

const person = new Person();
console.log(person.name);

// SyntaxError: Private field '#firstName' must be
// declared in an enclosing class
console.log(person.#firstName);
console.log(person.#lastName);

Note that the error thrown here is a syntax error, which happens at compile time, so no part of the code runs. The compiler doesn’t expect you to even try to access private fields from outside a class, so it assumes you’re trying to declare one.

3. await Operator at the Top Level

In JavaScript, the await operator is used to pause execution until a Promise is settled (fulfilled or rejected).

Previously, we could only use this operator in an async function – a function declared with the async keyword. We could not do so in the global scope.

function setTimeoutAsync(timeout) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve();
    }, timeout);
  });
}

// SyntaxError: await is only valid in async functions
await setTimeoutAsync(3000);

With ES2022, now we can:

function setTimeoutAsync(timeout) {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve();
    }, timeout);
  });
}

// Waits for timeout - no error thrown
await setTimeoutAsync(3000);

4. Static Class Fields and Static Private Methods

We can now declare static fields and static private methods for a class in ES13. Static methods can access other private/public static members in the class using the this keyword, and instance methods can access them using this.constructor.

class Person {
  static #count = 0;

  static getCount() {
    return this.#count;
  }

  constructor() {
    this.constructor.#incrementCount();
  }

  static #incrementCount() {
    this.#count++;
  }
}

const person1 = new Person();
const person2 = new Person();

console.log(Person.getCount()); // 2

5. Class static Block

ES13 allows the definition of static blocks that will be executed only once, at the creation of the class. This is similar to static constructors in other languages with support for object-oriented programming, like C# and Java.

A class can have any number of static {} initialization blocks in its class body. They will be executed, along with any interleaved static field initializers, in the order they are declared. We can use the super property in a static block to access properties of the super class.

class Vehicle {
  static defaultColor = 'blue';
}

class Car extends Vehicle {
  static colors = [];

  static {
    this.colors.push(super.defaultColor, 'red');
  }

  static {
    this.colors.push('green');
  }
}

console.log(Car.colors); // [ 'blue', 'red', 'green' ]

6. Ergonomic Brand Checks for Private Fields

We can use this new ES2022 feature to check if an object has a particular private field in it, using the in operator.

class Car {
  #color;

  hasColor() {
    return #color in this;
  }
}

const car = new Car();
console.log(car.hasColor()); // true;

The in operator is able to correctly distinguish private fields with the same names from different classes:

class Car {
  #color;

  hasColor() {
    return #color in this;
  }
}

class House {
  #color;

  hasColor() {
    return #color in this;
  }
}

const car = new Car();
const house = new House();

console.log(car.hasColor()); // true;
console.log(car.hasColor.call(house)); // false
console.log(house.hasColor()); // true
console.log(house.hasColor.call(car)); // false

7. at() Method for Indexing

We typically use square brackets ([]) in JavaScript to access the Nth element of an array, which is usually a simple process. We just access the N - 1 property of the array.

const arr = ['a', 'b', 'c', 'd'];
console.log(arr[1]); // b

However, we have to use an index of arr.length - N if we want to access the Nth item from the end of the array with square brackets.

const arr = ['a', 'b', 'c', 'd'];

// 1st element from the end
console.log(arr[arr.length - 1]); // d

// 2nd element from the end
console.log(arr[arr.length - 2]); // c

The new at() method added in ES2022 lets us do this in a more concise and expressive way. To access the Nth element from the end of the array, we simply pass a negative value of -N to at().

const arr = ['a', 'b', 'c', 'd'];

// 1st element from the end
console.log(arr.at(-1)); // d

// 2nd element from the end
console.log(arr.at(-2)); // c

Apart from arrays, strings and TypedArray objects also now have at() methods.

const str = 'Coding Beauty';
console.log(str.at(-1)); // y
console.log(str.at(-2)); // t

const typedArray = new Uint8Array([16, 32, 48, 64]);
console.log(typedArray.at(-1)); // 64
console.log(typedArray.at(-2)); // 48

8. RegExp Match Indices

This new feature allows us to specify that we want the get both the starting and ending indices of the matches of a RegExp object in a given string.

Previously, we could only get the starting index of a regex match in a string.

const str = 'sun and moon';

const regex = /and/;

const matchObj = regex.exec(str);

// [ 'and', index: 4, input: 'sun and moon', groups: undefined ]
console.log(matchObj);

Now with ES13, we can specify a d regex flag to get the two indices where the match starts and ends.

const str = 'sun and moon';

const regex = /and/d;

const matchObj = regex.exec(str);

/**
[
  'and',
  index: 4,
  input: 'sun and moon',
  groups: undefined,
  indices: [ [ 4, 7 ], groups: undefined ]
]
 */
console.log(matchObj);

With the d flag set, the object returned will have an indices property that contains the starting and ending indices.

9. Object.hasOwn() Method

In JavaScript, we can use the Object.prototype.hasOwnProperty() method to check if an object has a given property.

class Car {
  color = 'green';
  age = 2;
}

const car = new Car();

console.log(car.hasOwnProperty('age')); // true
console.log(car.hasOwnProperty('name')); // false

But there are certain problems with this approach. For one, the Object.prototype.hasOwnProperty() method is not protected – it can be overridden by defining a custom hasOwnProperty() method for a class, which could have completely different behavior from Object.prototype.hasOwnProperty().

class Car {
  color = 'green';
  age = 2;

  // This method does not tell us whether an object of
  // this class has a given property.
  hasOwnProperty() {
    return false;
  }
}

const car = new Car();

console.log(car.hasOwnProperty('age')); // false
console.log(car.hasOwnProperty('name')); // false

Another issue is that for objects created with a null prototype (using Object.create(null)), trying to call this method on them will cause an error.

const obj = Object.create(null);
obj.color = 'green';
obj.age = 2;

// TypeError: obj.hasOwnProperty is not a function
console.log(obj.hasOwnProperty('color'));

One way to solve these issues is to use to call the call() method on the Object.prototype.hasOwnProperty Function property, like this:

const obj = Object.create(null);
obj.color = 'green';
obj.age = 2;
obj.hasOwnProperty = () => false;

console.log(Object.prototype.hasOwnProperty.call(obj, 'color')); // true
console.log(Object.prototype.hasOwnProperty.call(obj, 'name')); // false

This isn’t very convenient. We can write our own reusable function to avoid repeating ourselves:

function objHasOwnProp(obj, propertyKey) {
  return Object.prototype.hasOwnProperty.call(obj, propertyKey);
}

const obj = Object.create(null);
obj.color = 'green';
obj.age = 2;
obj.hasOwnProperty = () => false;

console.log(objHasOwnProp(obj, 'color')); // true
console.log(objHasOwnProp(obj, 'name')); // false

No need for that though, as we can use the new built-in Object.hasOwn() method added in ES2022. Like our reusable function, it takes an object and property as arguments and returns true if the specified property is a direct property of the object. Otherwise, it returns false.

const obj = Object.create(null);
obj.color = 'green';
obj.age = 2;
obj.hasOwnProperty = () => false;

console.log(Object.hasOwn(obj, 'color')); // true
console.log(Object.hasOwn(obj, 'name')); // false

10. Error Cause

Error objects now have a cause property for specifying the original error that caused the error about to be thrown. This adds additional contextual information to the error and assists in the diagnosis of unexpected behavior. We can specify the cause of an error by setting a cause property on an object passed as the second argument to the Error() constructor.

function userAction() {
  try {
    apiCallThatCanThrow();
  } catch (err) {
    throw new Error('New error message', { cause: err });
  }
}

try {
  userAction();
} catch (err) {
  console.log(err);
  console.log(`Cause by: ${err.cause}`);
}

11. Array Find from Last

In JavaScript, we can already use the Array find() method to find an element in an array that passes a specified test condition. Similarly, we can use findIndex() to find the index of such an element. While find() and findIndex() both start searching from the first element of the array, there are instances where it would be preferable to start the search from the last element instead.

There are scenarios where we know that finding from the last element might achieve better performance. For example, here we’re trying to get the item in the array with the value prop equal to y. With find() and findIndex():

const letters = [
  { value: 'v' },
  { value: 'w' },
  { value: 'x' },
  { value: 'y' },
  { value: 'z' },
];

const found = letters.find((item) => item.value === 'y');
const foundIndex = letters.findIndex((item) => item.value === 'y');

console.log(found); // { value: 'y' }
console.log(foundIndex); // 3

This works, but as the target object is closer to the tail of the array, we might be able to make this program run faster if we use the new ES2022 findLast() and findLastIndex() methods to search the array from the end.

const letters = [
  { value: 'v' },
  { value: 'w' },
  { value: 'x' },
  { value: 'y' },
  { value: 'z' },
];

const found = letters.findLast((item) => item.value === 'y');
const foundIndex = letters.findLastIndex((item) => item.value === 'y');

console.log(found); // { value: 'y' }
console.log(foundIndex); // 3

Another use case might require that we specifically search the array from the end to get the correct item. For example, if we want to find the last even number in a list of numbers, find() and findIndex() would produce a totally wrong result:

const nums = [7, 14, 3, 8, 10, 9];

// gives 14, instead of 10
const lastEven = nums.find((value) => value % 2 === 0);

// gives 1, instead of 4
const lastEvenIndex = nums.findIndex((value) => value % 2 === 0);

console.log(lastEven); // 14
console.log(lastEvenIndex); // 1

We could call the reverse() method on the array to reverse the order of the elements before calling find() and findIndex(). But this approach would cause unnecessary mutation of the array, as reverse() reverses the elements of an array in place. The only way to avoid this mutation would be to make a new copy of the entire array, which could cause performance problems for large arrays.

Also, findIndex() would still not work on the reversed array, as reversing the elements would also mean changing the indexes they had in the original array. To get the original index, we would need to perform an additional calculation, which means writing more code.

const nums = [7, 14, 3, 8, 10, 9];

// Copying the entire array with the spread syntax before
// calling reverse()
const reversed = [...nums].reverse();

// correctly gives 10
const lastEven = reversed.find((value) => value % 2 === 0);

// gives 1, instead of 4
const reversedIndex = reversed.findIndex((value) => value % 2 === 0);

// Need to re-calculate to get original index
const lastEvenIndex = reversed.length - 1 - reversedIndex;

console.log(lastEven); // 10
console.log(reversedIndex); // 1
console.log(lastEvenIndex); // 4

It’s in cases like where the findLast() and findLastIndex() methods come in handy.

const nums = [7, 14, 3, 8, 10, 9];

const lastEven = nums.findLast((num) => num % 2 === 0);
const lastEvenIndex = nums.findLastIndex((num) => num % 2 === 0);

console.log(lastEven); // 10
console.log(lastEvenIndex); // 4

This code is shorter and more readable. Most importantly, it produces the correct result.

Conclusion

So we’ve seen the newest features ES13 (ES2022) brings to JavaScript. Use them to boost your productivity as a developer and write cleaner code with greater conciseness and clarity.

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 Resolve a Promise from Outside in JavaScript

To resolve a promise from outside in JavaScript, assign the resolve callback to a variable defined outside the Promise constructor scope, then call the variable to resolve the Promise. For example:

let promiseResolve;
let promiseReject;

const promise = new Promise((resolve, reject) => {
  promiseResolve = resolve;
  promiseReject = reject;
});

promiseResolve();

Now why would we need to do something like this? Well, maybe we have an operation A currently in progress, and the user wants another operation B to happen, but B must wait for A to complete. Let’s say we have a simple social app where users can create, save and publish posts.

index.html

<!DOCTYPE html>
<html>
  <head>
    <title>Resolving a Promise from Outside</title>
  </head>
  <body>
    <p>
      Save status:
      <b><span id="save-status">Not saved</span></b>
    </p>
    <p>
      Publish status:
      <b><span id="publish-status">Not published</span></b>
    </p>
    <button id="save">Save</button>
    <button id="publish">Publish</button>
    <script src="index.js"></script>
  </body>
</html>
A simple app where users can create, save and publish posts.
Publish doesn’t happen until after save.

What if a post is currently being saved (operation A) and the user wants to publish the post (operation B) while saving is ongoing?. If we don’t want to disable the “Publish” button when the save is happening, we’ll need to ensure the post is saved before publish happens.

index.js


// Enable UI interactivity
const saveStatus = document.getElementById('save-status');
const saveButton = document.getElementById('save');
const publishStatus = document.getElementById(
  'publish-status'
);
const publishButton = document.getElementById('publish');
saveButton.onclick = () => {
  save();
};
publishButton.onclick = async () => {
  await publish();
};

let saveResolve;
let hasSaved = false;

function save() {
  hasSaved = false;
  saveStatus.textContent = 'Saving...';
  setTimeout(() => {
    saveResolve();
    hasSaved = true;
    saveStatus.textContent = 'Saved';
  }, 3000);
}

async function waitForSave() {
  if (!hasSaved) {
    await new Promise((resolve) => {
      saveResolve = resolve;
    });
  }
}

async function publish() {
  publishStatus.textContent = 'Waiting for save...';
  await waitForSave();
  publishStatus.textContent = 'Published';
  return;
}

The key parts of this code are the save() and waitForSave() functions. When the user clicks “Publish”, waitForSave() is called. If the post has already been saved, the Promise returned from waitForSave() resolves immediately, otherwise it assigns its resolve callback to an external variable that will be called after the save. This makes publish() wait for the timeout in save() to expire before continuing.

Post is saved before publish happens.

We can create a Deferred class to abstract and reuse this logic:

class Deferred {
  constructor() {
    this.promise = new Promise((resolve, reject) => {
      this.reject = reject;
      this.resolve = resolve;
    });
  }
}

const deferred = new Deferred();

// Resolve from outside
deferred.resolve();

Now the variables to resolve/reject a Promise and the Promise itself will be contained in the same Deferred object.

We can refactor our code to use this class:

// Enable UI interactivity
// ...

const deferredSave = new Deferred();
let hasSaved = false;

function save() {
  hasSaved = false;
  saveStatus.textContent = 'Saving...';
  setTimeout(() => {
    deferredSave.resolve();
    hasSaved = true;
    saveStatus.textContent = 'Saved';
  }, 3000);
}

async function waitForSave() {
  if (!hasSaved) await deferredSave.promise;
}

async function publish() {
  // ...
}

And the functionality will work as before:

The functionality works as before after the refactor.