Published on

User Authentication With React

Authors
  • avatar
    Name
    Jacob Toftgaard Rasmussen
    Twitter

Link to part 1 of the guide

For part 2 of the guide we will create a small web app using React and Typescript. It will be very simple and consist of just three components:

App.tsx
LoginForm.tsx
RegisterForm.tsx

When we are done the app will look like this, and I think that the interface talks for itself.

Screenshot of final UI

Of course there are many ways that we could make it prettier, but the focus of this article is to establish communication to the API from the previous articles.
Are you ready? Then let's get started!

We can use Vite to quickly get our app scaffolded and running. Don't worry if you have not used Vite before, it is super simple and I will show you what to do. You can also check out the official documentation if you are curious.

Open a terminal and navigate to a directory where you want to create your frontend project. Then if you use NPM as package manager use the following command:

npm create vite@latest

If you use Yarn instead then execute the following command:

yarn create vite

In the following prompts, provide your project with a name, choose React as the framework and choose Typescript as the variant.
Next, cd into the newly created project directory and execute the commands for NPM users:

npm install

npm run dev

Or these for yarn users

yarn install

yarn dev

This will start your frontend app, and open up a browser window showing the default template. It should look like this:

Screenshot the default Vite template

The first step is to open up the App.tsx file inside the src directory. The goal for the app is to access the secure weather forecast that the ASP.NET Core backend exposes, so let's start by adding the code that will do exactly that. Inside the function called App() delete everything in between the curly brackets, and exchange it with this code instead:

function App() {
  const [weather, setWeather] = useState<weatherType>({
    summary: undefined,
    temperature: undefined,
  });

  const getWeather = async () => {
    const response = await fetch("http://localhost:5010/weatherforecast", {});
    const data = await response.json();

    setWeather({
      summary: data[0].summary,
      temperature: data[0].temperatureC,
    });
  };

  return (
    <div className="App">
      <div style={{ marginTop: "75px" }}>
        <button onClick={getWeather} style={{ marginBottom: "10px" }}>
          Get secret weather forecast
        </button>
        <div>
          <div>Weather summary: {weather.summary}</div>
          <div>Weather temperature: {weather.temperature}</div>
        </div>
      </div>
    </div>
  );
}

You will also need to add this import at the top of the file:

import { useState } from "react";

Now your app should look like this:

Screenshot of the UI with the button, weather summary and temperature

Now the App component includes three things. Firstly, we have a useState statement that creates a variable called weather and a function called setWeather. weather will store the state of the weather forecast, and at the beginning it will simply be undefined. setWeather is a function that will allow us to assign weather with a new state.
Secondly, we have another function called getWeather. This function will make an asynchronous fetch call to our API's weatherforecast endpoint, (remember to set the port such that it matches your local setup). Once it has received a response from the backend, we will store the weather forecast in the weather variable using the setWeather function.
Thirdly, we have the HTML that the App component consists of. Here we have a button with an onClick attribute on it, which will call the getWeather function. Then we also have two div tags that just display the current state of the weather variable.

If you open up the developer console in your browser, and then click on the button, you should receive a 401 error response from the API. That's good, because that means that our authentication for the weather forecast endpoint is working.

Creating forms for registration and for signing in

The next step is to provide users some way of signing up a new account, and logging in to an existing account. To do this, we will create two new components: RegisterForm.tsx and LoginForm.tsx. RegisterForm.tsx looks like this:

import { useState } from "react";

const RegisterForm = () => {
  const [email, setEmail] = useState<string>("");
  const [username, setUsername] = useState<string>("");
  const [password, setPassword] = useState<string>("");

  type RegistrationRequest = {
    email: string;
    username: string;
    password: string;
  };

  const registerUser = async () => {
    if (email && username && password) {
      const regRequest: RegistrationRequest = {
        email,
        username,
        password,
      };
      const response = await fetch("http://localhost:5010/auth/register", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify(regRequest),
      });
      const data = await response.json();
      console.log(data);
    }
  };

  return (
    <div>
      <h1>Register</h1>
      <form action="#">
        <div>
          <input
            type="email"
            name="email"
            placeholder="Email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
          />
        </div>

        <input
          type="username"
          name="username"
          placeholder="Username"
          value={username}
          onChange={(e) => setUsername(e.target.value)}
        />

        <div>
          <input
            type="password"
            name="password"
            placeholder="Password"
            value={password}
            onChange={(e) => setPassword(e.target.value)}
          />
        </div>

        <div>
          <button
            type="button"
            onClick={registerUser}
            style={{ marginTop: "10px" }}
          >
            Submit
          </button>
        </div>
      </form>
    </div>
  );
};

export default RegisterForm;

There is nothing fancy going on here, we just have a form tag with three input tags inside of it. Similarly to the weather variable that we saw previously in the App.tsx component, we also use the useState hook to create state variables for the email, username and password variables. The function called registerUser first checks that the email, username and password are all present and if they are then it sends a fetch request to the API's registration endpoint. That function is triggered when we click the Submit button because of the onClick attribute that we put on the button. We also define a type for the RegistrationRequest for type safety.

The next file is the LoginForm.tsx and it looks like this:

import { useState } from "react";

const LoginForm = () => {
  const [email, setEmail] = useState<string>("");
  const [password, setPassword] = useState<string>("");

  type LoginRequest = {
    email: string;
    password: string;
  };

  const login = async () => {
    if (email && password) {
      const loginRequest: LoginRequest = {
        email,
        password,
      };
      const response = await fetch("http://localhost:5010/auth/login", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify(loginRequest),
      });
      const data = await response.json();
    }
  };

  return (
    <div className="LoginForm">
      <h1>Login</h1>
      <form action="#">
        <div>
          <input
            type="email"
            name="email"
            placeholder="Email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
          />
        </div>

        <div>
          <input
            type="password"
            name="password"
            placeholder="Password"
            value={password}
            onChange={(e) => setPassword(e.target.value)}
          />
        </div>

        <div>
          <button type="button" onClick={login} style={{ marginTop: "10px" }}>
            Login
          </button>
        </div>
      </form>
    </div>
  );
};

export default LoginForm;

This component is so similar to the RegisterForm that I will not explain it. Take a look at the code, and I am sure that you understand everything going on here.
Next, let's use these two components inside of the App component, so open up App.tsx and add the following code:

<RegisterForm />
<LoginForm />

Just inside the div tag with the className="App".
Great! The interface of the your app should now look like the initial picture in this guide. Try to open up the developer console and go to the network tab, then first try to register a new user, and afterwards login with that user, (you need to have the backend API running as well as the docker container with the PostgreSQL database). In the network tab you should see two successful calls. The first has a response 201 because it created a new user, and the second has a response 200 also indicating a successful request. If you further inspect the second response, by clicking on it, you will find the JWT access token that the user can use to request the weather forecast. It should look like the screenshot below:

Screenshot the network tab

Using the access token in the weather forecast request

The final step that ties it all together is to use the access token when we make calls to the weather forecast. Currently, the token is not taken care of when we receive it after logging in. To fix this we will create a new state variable inside App.tsx. Just above the declaration of weather and setWeather add the following line:

const [token, setToken] = useState<string>("");

This variable will hold the token, and initially it will just be an empty string. When we receive the access token after logging in, then we will use the setToken function to overwrite the token variable with the new token. In order to do this when we receive the login response we have to pass setToken to the Login component as part of its props. First, let's add the needed code to Login.tsx. Add a parameter to the LoginForm function such that it looks like this:

const LoginForm = (props: {
  setToken: React.Dispatch<React.SetStateAction<string>>
}) => {

This specifies that we need to provide the setToken function when we use the LoginForm component. Also, inside the function called login just after the line with const data = await response.json(), add the following line of code:

props.setToken(data.token);

Then inside App.tsx you need to pass the setToken function to the LoginForm component like this:

<LoginForm setToken={setToken} />

Finally, we just need to use the token in our request to the weather API. Also in App.tsx find the getWeather function. We need to add authorization headers to the fetch request. Exchange the line:

const response = await fetch("http://localhost:5010/weatherforecast", {});

With this code:

const response = await fetch("http://localhost:5010/weatherforecast", {
  headers: {
    Authorization: `Bearer ${token}`,
  },
});

That is it!
Now click on the Get secret weather forecast button, and you should get the weather forecast just like this:

Screenshot the network tab

Congratulations on getting this far! I hope you enjoyed this full stack guide, and that it was useful for you.