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>
© 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>
© 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>
© 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.