Example Source Code

Spring Boot Server

A new example program

In this lecture I am going to be introducing some new features in React, including routes and contexts. To help me demonstrate these new features I am going to be building a simple example program. This program consists of a Spring Boot back end and database and, of course, a React app for the front end.

This application is going to be fairly minimal, but can serve as the starting point for any number of projects that need to use the features I have implemented here.

The example project is a single page React application that gives the appearance of having two pages: a home page and a profile page.

When the application starts up the user will see the home page. Since many of the features of this app will require the user to be logged in, the home page will give the user the opportunity to log in to an existing account or create a new one.

At the top of the page we have a navigation bar with links that the user can use to navigate to the different pages in the app. Clicking on the Profile link in the navigation bar takes you to the profile page where you can either create a new user profile or edit your exising profile.

A new security model

Many real world applications require users to be logged in before performing any actions. This example application follows that pattern. For example, users can not create or modify profiles until they have logged in.

The back end server will be using a security mechanism to ensure that users are properly logged in before performing most actions. The mechanism uses a special security token, called a Java Web Token, or JWT, to check that users have permissions to perform actions.

When a user logins in successfully or creates a new account, the server will send us a JWT in the body of the response. A JWT is simply a long, complex text string that contains encrypted information about the user.

To perform subsequent actions on the server we will have to submit the JWT with each request. We do this by adding a special header to each subsequent request that takes the form

Authorization: Bearer <JWT>

with <JWT> replaced by the JWT the server sent to us.

One advantage of using JWTs that that they contain useful information embedded in the encrypted JWT. For example, this application stores the id number of the user inside the JWT. When we send the JWT back to the server the server can both confirm that we are legitimate user who has logged in properly and can see what our user id number is. This saves us from having to send the user id as part of a request. For example, to fetch a user's profile we will simply use the URL

http://localhost:8085/profile

Note that this URL contains no information about the user: that information is instead embedded in the authorization header we will be embedding in the request.

Working with Routes

Our application is going to be a single page React app that creates the illusion of multiple distinct pages. We have already seen a couple of techniques for creating this illusion. In a traditional JavaScript application we can use the trick of creating multiple divs and then systematically hiding and showing those divs as the user progresses through the application. In React we have seen that you can use conditional rendering to render or not render components based on the application's current state. Neither of these techniques scale well to large applications. As third, more scalable alternative we can use routes.

To implement routes in our React application we need to start by adding a library to our application that offers a set of routing components. For this example I have chosen the react-router-dom package. To add this package to our project we run the command

npm install react-router-dom

in the terminal.

This package offers a number of new components which work together to implement routes in our application. Here is some code from our App component to demonstrate how this works:

<BrowserRouter>
<nav>
  <Link to="/">Home</Link>{' '}
  <Link to="/profile">Profile</Link>{' '}
</nav>
<Routes>
  <Route path="/" element={<Home setJwt={setJwt} />} />
  <Route path="/profile" element={<Profile />} />
</Routes>
</BrowserRouter>

The first component we use here is the BrowserRouter. This wraps all of the routing functionality in our app. The App component shown here consists of a navigation bar and a content region.

We implement the navigation bar via an HTML nav element. The links displayed in the navigation bar use a Link component provided by the react-router-dom package. Each link has a to property that specifies the route the Link will activate.

Inside the content area of the app we see a Routes element containing a list of Route elements. Each Route specifies a make believe page consisting of a route path and an element that will be the top level element for that simulated page. To implement the Home and Profile pages here I have written Home and Profile components.

The "/" route path is the default path for our application. When the app first loads the routing system will be using that default path, which in turn means that the first component we will display in our app is the Home component.

Working with the JWT and a Context

Since the back end server will be making extensive use of JWTs, we need to construct our React app to manage the JWT and make sure it is available where it is needed.

To start this process I will add a state variable in the App component that stores the current JWT:

const [jwt,setJwt] = useState('');

Since the Home component will be handling the log in process and thus will be receiving the JWT from the server, we will pass the setJwt function to the Home component as a prop so it can set the App's jwt state variable after logging in.

The Profile component will also need to have access to the JWT, since it will need to send the JWT to the server in a header along with each request it sends. As you can see from the code above, we are not going to be sending the current JWT to the Profile in a prop. Instead, we will be making use of a new mechanism called a React context to send this information down to elements who need them.

The context mechanism starts with a call to the React useContext() hook. I put this code in a separate file, AuthContext.jsx:

import { createContext } from "react";

const AuthContext = createContext('');

export default AuthContext;

Components that need to interact with this context will need to import it from this module.

A React context is an object containing a number of important properties. The first of these we will be using in our App component is a provider which uses the context mechanism to pass a value down to its child components:

<AuthContext.Provider value={jwt}>
  <BrowserRouter>
    <nav>
      <Link to="/">Home</Link>{' '}
      <Link to="/profile">Profile</Link>{' '}
    </nav>
    <Routes>
      <Route path="/" element={<Home setJwt={setJwt} />} />
      <Route path="/profile" element={<Profile />} />
    </Routes>
  </BrowserRouter>
</AuthContext.Provider>

We will be using the provider to pass the current JWT value from our app to any of the subcomponents that need it.

Here now is some of the code for the Profile component to show how that component can then read the JWT from the context:

import { useContext } from "react";
import AuthContext from "./AuthContext";

function Profile() {
    const jwt = useContext(AuthContext);  

Error handling in fetch()

In previous examples I have tried to keep any fetch code we have used as simple as possible. Those early examples were a little bit too simple, in that they ignored important things such as error handling. Since we are now dealing with an application where more things can potentially go wrong, we need to add some error handling code to our fetch() requests.

In all of the examples we have seen so far we have been doing things like this:

fetch(<URL>).then(response => response.json()).then(<further processing>);

We now need to think more carefully about what we do in the first .then() clause. The function that we pass to that clause will be receiving a response object as its parameter: this object contains useful information about the result of the request, such as response codes and data from the server. If we are going to be doing proper error checking we need to pass that first then() clause a function that will look carefully at the response and respond correctly when something goes wrong.

To make it easy to do error checking in my fetch() requests, I started by defining some special processing functions in a separate module, FetchRoutines.js. Here is the code for one of those routines:

export function processJSON(response) {
    if(response.ok) 
        return response.json();
    if(response.status == 401) // Not authorized
        alert("Your login has expired. Please log in again.");
    return Promise.reject(response);
}

This function does more careful error checking. The first thing it does is to check the response object's ok property: this will be true if the fetch request was successful. If the request was not successful, we next can check the status code of the response to check for common server errors we can anticipate ahead of time. One problem I could anticipate here is that the user's JWT may have expired. One security feature of the JWTs the server sends us is that they expire after an hour. When we send an expired JWT to the back end in a request the server will respond with a HTTP 401, Not Authorized, status code. This processing function checks for that specific error and puts up an alert when it sees it.

With this function we can now do our fetch() requests more like this:

fetch(<URL>).then(processJSON).then(<further processing>);

The processJSON() function will take care of the error checking and first round processing of the response. If the request is successful we will go ahead and run the code in the following .then() clause. In case of an error, the processing will abort, as it should.