Basic Routing

The first step is to implement some basic routing functionality. Unlike Ember, React does not come with routing functionality built in. We thus need to install some additional packages to implement routing:

$ yarn add react-router-dom

The first route we will implement is the login page. Create a new file src/js/routes/login.jsx and then add the following content:

import React, { useState } from "react";

function Login() {
  const [email, setMail] = useState("");
  const [password, setPassword] = useState("");
  const [loginValid, setLoginValid] = useState(true);

  const updateEmail = (ev) => {
    setMail(ev.target.value);
  };

  const updatePassword = (ev) => {
    setPassword(ev.target.value);
  };

  const handleLogin = (ev) => {
    ev.preventDefault();
    if (email === "test@example.com" && password === "password") {
      setLoginValid(true);
    } else {
      setLoginValid(false);
    }
  };

  let errorTag = null;
  let errorLabel = null;
  let errorInput = null;
  if (!loginValid) {
    errorTag = (
      <span className="invalid-feedback">
        No user exists with the e-mail address {email} or the password is
        incorrect.
      </span>
    );
    errorLabel = "text-danger";
    errorInput = "form-control is-invalid";
  } else {
      errorLabel = null;
      errorTag = null;
      errorInput = "form-control"
  }

  return (
    <main className="row justify-content-lg-center">
      <form className="col-6" action="index.html" onSubmit={handleLogin}>
        <h1>Login</h1>
        <div className="form-group">
          <label className={errorLabel}>
            E-Mail Address
            <input
              className={errorInput}
              type="email"
              name="email"
              placeholder="Your e-mail address"
              tabIndex="1"
              required="required"
              value={email}
              onChange={updateEmail}
            />
            {errorTag}
          </label>
        </div>
        <div className="form-group">
          <label className={errorLabel}>
            Password
            <input
              className={errorInput}
              type="password"
              name="password"
              placeholder="Your password"
              tabIndex="2"
              required="required"
              value={password}
              onChange={updatePassword}
            />
            {errorTag}
          </label>
        </div>
        <div className="btn-container">
          <button role="button" tabIndex="3">
            Login
          </button>
        </div>
      </form>
    </main>
  )
}

export default Login;

This is the same code that in the first tutorial we had in the athena.jsx file. Update the athena.jsx to remove the code we just copied, so that the file looks like this:

import React from 'react';
import ReactDOM from 'react-dom';

import AriaMenu from './components/aria-menu.jsx';
import Login from './routes/login.jsx';
import '../styles/app.scss';

function Athena {
  return (
    <div className="container-fluid gx-0">
      <header className="row gx-0">
        <nav className="navbar navbar-dark bg-dark" aria-label="Main">
          <AriaMenu class="navbar-nav horizontal" aria-label="Main">
            <li className="navbar-brand">Athena - Study Portal</li>
            <li className="nav-item">
              <a
                href=""
                role="menuitem"
                tabIndex="0"
                className="nav-link active"
              >
                My Modules
              </a>
            </li>
            <li className="nav-item">
              <a href="" role="menuitem" tabIndex="-1" className="nav-link">
                My Exams
              </a>
            </li>
          </AriaMenu>

          <AriaMenu class="navbar-nav horizontal" aria-label="User">
            <li className="nav-item">
              <a
                href="profile.html"
                className="nav-link"
                role="menuitem"
                tabIndex="0"
              >
                Profile
              </a>
            </li>
            <li className="nav-item">
              <a
                href="login.html"
                className="nav-link"
                role="menuitem"
                tabIndex="-1"
              >
                Logout
              </a>
            </li>
          </AriaMenu>
        </nav>
      </header>
      <Login/>
      <footer>
        &copy; 2018 - Mark Hall (<a href="mailto:mark.hall@informatik.uni-halle.de" tabIndex="0">mark.hall@informatik.uni-halle.de</a>)
      </footer>
    </div>
  )
}

ReactDOM.render(<Athena/>, document.getElementById("app-entry-point"));
}

The main changes are that we import the Login component and that we then output that component to generate the application. At this point we do not have any routing functionality and in order to implement that, we included an option to our devServer section of``webpack.conf.js`` last week. The option was historyApiFallback and as a reminder, it should look like that:

...
devServer: {
  static: {
    directory: path.resolve(__dirname, "dist"),
  },
  open: false,
  historyApiFallback: true,
},
...

The reason we needed to include this, is that when we implement the routing functionality, this will update the URL that is shown in the browser. When we then update the code and re-load the page, the browser will try and re-load the updated URL, but this URL does not actually exist for the web-server, it only exists within the React application itself. By setting the historyApiFallback option, we are basically telling the web-server that all requests should always be mapped to the index.html file and that the JavaScript code will take care of distinguishing the different routes of the application. The second change we need to make is to the index.html template, adding the <base> tag:

<head>
  <meta charset="utf-8"/>
  <title><%= htmlWebpackPlugin.options.title %></title>
  <link rel="stylesheet" href="https://cdn.materialdesignicons.com/2.8.94/css/materialdesignicons.min.css"/>
  <% for (var css in htmlWebpackPlugin.files.css) { %>
    <link href="<%= htmlWebpackPlugin.files.css[css] %>" rel="stylesheet"/>
  <% } %>
  <base href="/"/>
</head>

The <base> tag defines the base URL, from which all relative URLs, such as the ones in the <link> or <script> tags are generated are interpreted. Use / as the base URL ensures that our CSS and JS loads correctly, even when we have navigated to a more specific route.

With that in place, we can add in our first route. First, we need to import some extra code into our athena.jsx:

import React from 'react';
import ReactDOM from 'react-dom';
import { BrowserRouter as Router, Routes, Route, Link } from "react-router-dom";
...

The first import BrowserRouter implements the URL-based routing. The Routes and Route components are used to actually define the route structure and the Link component is used to generate links to the routes. Next, we update the return statement to use the Router:

return (
  <Router>
    <div className="container-fluid gx-0">
      <header className="row gx-0">
        <nav className="navbar navbar-dark bg-dark" aria-label="Main">
          <AriaMenu class="navbar-nav horizontal" aria-label="Main">
            <li className="navbar-brand">Athena - Study Portal</li>
            <li className="nav-item">
              <a
                href=""
                role="menuitem"
                tabIndex="0"
                className="nav-link active"
              >
                My Modules
              </a>
            </li>
            <li className="nav-item">
              <a href="" role="menuitem" tabIndex="-1" className="nav-link">
                My Exams
              </a>
            </li>
          </AriaMenu>

          <AriaMenu class="navbar-nav horizontal" aria-label="User">
            <li className="nav-item">
              <Link
                to="/login"
                className="nav-link"
                role="menuitem"
                tabIndex="0"
              >
                Login
              </Link>
            </li>
            <li className="nav-item">
              <a
                href="login.html"
                className="nav-link"
                role="menuitem"
                tabIndex="-1"
              >
                Logout
              </a>
            </li>
          </AriaMenu>
        </nav>
      </header>
      <Routes>
        <Route path="/login" element={<Login />} />
      </Routes>
      <footer>
        &copy; 2018 - Mark Hall (
        <a href="mailto:mark.hall@informatik.uni-halle.de" tabIndex="0">
          mark.hall@informatik.uni-halle.de
        </a>
        )
      </footer>
    </div>
  </Router>
);

The important thing is that the Router must wrap the whole application. Then inside we can use the Routes to define a block of routes, and inside that use the Route to define a specific route. The <Route> must have at least the two parameters shown here. The path defines the URL path that route matches. The element defines the component that is rendered, if the URL matches the path. Be aware that you need to invoke the component with the sharp brackets. If you left them out, you would receive an error, informing you, that functions are no valid React children. This error helps by stating, that this may be an issue since you tried to pass Component instead of <Component />.

Second, if you look into the <AriaMenu>, we use the <Link> to generate a URL to the path of our route. With that in place, you can now click on the “Login” link, and the login route will be shown. The login screen will not be shown, however, if we are at the root URL.

Nesting Routes

In the next step we will look at creating more complex route structures to handle the modules functionality. Create a new file routes/modules.jsx and add the following code:

import React from "react";
import { Routes, Route } from "react-router-dom";

import Index from './modules/index.jsx';

export default function Modules() {
    return (
        <Routes>
          <Route index element={<Index />}/>
        </Routes>
    );
};

In this use of <Route>, we use index instead of a path to specify that this route should be rendered if the URL path matches the parent’s URL. This is because later we will add more routes that start with “/modules”, but which should match the more specific routes, rather than this generic route. Actually, the parent’s route Outlet is rendered.

Next, create a new file routes/modules/index.jsx and add the following code:

import React from "react";

export default function Index() {
  /**
  * Render the component
  */
  return (
    <div>
      <h1>My Modules</h1>
      <table>
        <thead>
          <tr>
            <th>Code</th>
            <th>Name</th>
            <th>Sections</th>
            <th>Actions</th>
          </tr>
        </thead>
        <tbody>
          <tr>
            <td>
              <a href="">INF.06528.01</a>
            </td>
            <td>
              <a href="">Client-seitige Web-Anwendungen</a>
            </td>
            <td>
              <nav aria-label="Sections of Client-seitige Web-Anwendungen">
                <ul role="menu" className="horizontal" aria-label="Sections">
                  <li>
                    <a
                      href=""
                      role="menuitem"
                      aria-label="Dates"
                      title="Dates"
                      className="mdi mdi-calendar-clock"
                      tabIndex="0"
                    ></a>
                  </li>
                  <li>
                    <a
                      href=""
                      role="menuitem"
                      aria-label="Documents"
                      title="Documents"
                      className="mdi mdi-file-document-box-multiple-outline"
                      tabIndex="-1"
                    ></a>
                  </li>
                  <li>
                    <a
                      href=""
                      role="menuitem"
                      aria-label="Exercises"
                      title="Exercises"
                      className="mdi mdi-test-tube"
                      tabIndex="-1"
                    ></a>
                  </li>
                  <li>
                    <a
                      href=""
                      role="menuitem"
                      aria-label="Teilnehmerinnen"
                      title="Teilnehmer_innen"
                      className="mdi mdi-account-multiple"
                      tabIndex="-1"
                    ></a>
                  </li>
                </ul>
              </nav>
            </td>
            <td>
              <nav aria-label="Actions for Client-seitige Web-Anwendungen">
                <ul role="menu" className="horizontal" aria-label="Actions">
                  <li>
                    <a
                      href=""
                      role="menuitem"
                      aria-label="Leave"
                      title="Leave"
                      className="mdi mdi-delete warning"
                      tabIndex="0"
                    ></a>
                  </li>
                </ul>
              </nav>
            </td>
          </tr>
          <tr>
            <td>
              <a href="">INF.05370.01</a>
            </td>
            <td>
              <a href="">Informatik in den Geistes- und Kulturwissenschaften</a>
            </td>
            <td>
              <nav aria-label="Sections of Informatik in den Geistes- und Kulturwissenschaften">
                <ul role="menu" className="horizontal" aria-label="Sections">
                  <li>
                    <a
                      href=""
                      role="menuitem"
                      aria-label="Dates"
                      title="Dates"
                      className="mdi mdi-calendar-clock"
                      tabIndex="0"
                    ></a>
                  </li>
                  <li>
                    <a
                      href=""
                      role="menuitem"
                      aria-label="Documents"
                      title="Documents"
                      className="mdi mdi-file-document-box-multiple-outline"
                      tabIndex="-1"
                    ></a>
                  </li>
                  <li>
                    <a
                      href=""
                      role="menuitem"
                      aria-label="Exercises"
                      title="Exercises"
                      className="mdi mdi-test-tube"
                      tabIndex="-1"
                    ></a>
                  </li>
                  <li>
                    <a
                      href=""
                      role="menuitem"
                      aria-label="Teilnehmerinnen"
                      title="Teilnehmer_innen"
                      className="mdi mdi-account-multiple"
                      tabIndex="-1"
                    ></a>
                  </li>
                </ul>
              </nav>
            </td>
            <td>
              <nav aria-label="Actions for Informatik in den Geistes- und Kulturwissenschaften">
                <ul role="menu" className="horizontal" aria-label="Actions">
                  <li>
                    <a
                      href=""
                      role="menuitem"
                      aria-label="Leave"
                      title="Leave"
                      className="mdi mdi-delete warning"
                      tabIndex="0"
                    ></a>
                  </li>
                </ul>
              </nav>
            </td>
          </tr>
          <tr>
            <td>
              <a href="">INF.06450.01</a>
            </td>
            <td>
              <a href="">eHumanities Grundlagen</a>
            </td>
            <td>
              <nav aria-label="Sections of eHumanities Grundlagen">
                <ul role="menu" className="horizontal" aria-label="Sections">
                  <li>
                    <a
                      href=""
                      role="menuitem"
                      aria-label="Dates"
                      title="Dates"
                      className="mdi mdi-calendar-clock"
                      tabIndex="0"
                    ></a>
                  </li>
                  <li>
                    <a
                      href=""
                      role="menuitem"
                      aria-label="Documents"
                      title="Documents"
                      className="mdi mdi-file-document-box-multiple-outline"
                      tabIndex="-1"
                    ></a>
                  </li>
                  <li>
                    <a
                      href=""
                      role="menuitem"
                      aria-label="Exercises"
                      title="Exercises"
                      className="mdi mdi-test-tube"
                      tabIndex="-1"
                    ></a>
                  </li>
                  <li>
                    <a
                      href=""
                      role="menuitem"
                      aria-label="Teilnehmerinnen"
                      title="Teilnehmer_innen"
                      className="mdi mdi-account-multiple"
                      tabIndex="-1"
                    ></a>
                  </li>
                </ul>
              </nav>
            </td>
            <td>
              <nav aria-label="Actions for eHumanities Grundlagen">
                <ul role="menu" className="horizontal" aria-label="Actions">
                  <li>
                    <a
                      href=""
                      role="menuitem"
                      aria-label="Leave"
                      title="Leave"
                      className="mdi mdi-delete warning"
                      tabIndex="0"
                    ></a>
                  </li>
                </ul>
              </nav>
            </td>
          </tr>
        </tbody>
      </table>
    </div>
  );
}

The last step is to add the route to the athena.jsx:

...
import Login from './routes/login.jsx';
import Modules from './routes/modules.jsx';
...
return (
  <Router>
    <div className="container-fluid gx-0">
      <header className="row gx-0">
        <nav className="navbar navbar-dark bg-dark" aria-label="Main">
          <AriaMenu class="navbar-nav horizontal" aria-label="Main">
            <li className="navbar-brand">Athena - Study Portal</li>
            <li className="nav-item">
              <Link
                to="/modules"
                role="menuitem"
                tabIndex="0"
                className="nav-link active"
              >
                My Modules
              </Link>
            </li>
            <li className="nav-item">
              <a href="" role="menuitem" tabIndex="-1" className="nav-link">
                My Exams
              </a>
            </li>
          </AriaMenu>

          <AriaMenu class="navbar-nav horizontal" aria-label="User">
            <li className="nav-item">
              <Link
                to="/login"
                className="nav-link"
                role="menuitem"
                tabIndex="0"
              >
                Login
              </Link>
            </li>
            <li className="nav-item">
              <a
                href="login.html"
                className="nav-link"
                role="menuitem"
                tabIndex="-1"
              >
                Logout
              </a>
            </li>
          </AriaMenu>
        </nav>
      </header>
      <Routes>
        <Route path="login" element={<Login />} />
        <Route path="modules/*" element={<Modules />} />
      </Routes>
      <footer>
        &copy; 2018 - Mark Hall (
        <a href="mailto:mark.hall@informatik.uni-halle.de" tabIndex="0">
          mark.hall@informatik.uni-halle.de
        </a>
        )
      </footer>
    </div>
  </Router>
);

One of the things that we have to take into account when creating nested <Route> with a path, then on the outer level, we have <Route path="modules/*" element={Modules}/>, thus if the URL path starts with “/modules”, the Modules component is displayed. Inside the Modules component, we have another <Route> with the path set to “/modules” again, but this time loading the Index component. This is because the information of what part of the URL path have matched the parent Route is not passed on to the nested Route, thus we always have to check the full URL path.

We have also replaced the <a>My Modules</a> with a <Link> to create the needed link, which you can now try to use to navigate between the modules list and the login page.