DEV Community

MartinJ
MartinJ

Posted on • Edited on

6.1 Polishing your Firebase webapp - What's a 'Single-Page' Webapp? Learn about React-Router V6 (and Vite).

Last reviewed: Mar 2023

This post is part of a series designed to provide a very basic introduction to commercial software development practices. It might be a bit wordy for some tastes and isn't tremendously helpful about matters of detail. But if you're just trying to get your head around some of the broad issues that are described here and want to home in on key coding patterns that handle them, you might find it useful. If not, please feel free to tell me and I'll try to do better!

For a full index to the series see the Waypoints index at ngatesystems.com.

1. Introduction

When you configured your first Firebase project you might remember that Firebase wanted to know whether or not this was going to be a "single-page" app. Now it's time to find out why the recommended answer was "yes" and to find out how you handle the consequences.

For a start, what exactly is meant by a "page" in this context? To the user of a webapp, a "page" is a screenful of information all related to a particular topic. For example, on a sales site, the user might expect to find "product-detail" and "contact" pages.

But there's much more mileage in the page concept than creating a neat package for a group of information. For example, if a page within a site is keyed by some sort of unique address - say a key formed by adding a /pageName suffix to the site's basic url - then users can bookmark this and save it for future reference. This is a huge convenience - indispensable, in fact. Also, if users have access to a record of pages visited through their use of a webapp, they may use this to effortlessly unwind their track through the system - a big help to comprehension. Bookmarking and page history capabilities are built into web browsers. Users get very cross if these don't work as they expect.

So, all ways up, "pages" are going to be an essential part of any webapps' design. But how can a "single-page" webapp offer multiple pages?

A word of history here. In the dim and distant, each of the addresses for a webapp's pages would have had its own tailor-made file. This would have been designed specifically to handle that page's functionality. What was wrong with this? Why bother with this "single-page" nonsense at all? Let me tell you that, while this was fine for users, it was an absolute nightmare for the IT developer. It's not so long ago that my own code was a perfect jungle of server-based PHP scripts linked together by "header" instructions. No longer though, thank goodness!

So what you have to do if you want to enjoy the simplicity of using a "single-page" webapp is to find a way of simulating multi-page operation.

Let's look at what you have to do in order to achieve this. Suppose your webapp is deployed at https://mywebapp.com and initially offers just a "menu" page providing clickable links to more expansive "about" and "contributors" pages. When you click one of these - the "about link, say - you need to perform the following actions:

  • replace the current screen display with the "about" page (obviously!).

     

  • then, in order to enable the user to create a bookmark for the "about" page you need to change the browser's url field to https://mywebapp.com/about

     

  • and then, in order to enable the user to return to https://mywebapp.com by clicking the browser's "back" button you must make appropriate adjustments to the browser's "page-history" record.

     

  • and finally, you need to think about one overriding issue. If a user has created an https://mywebapp.com/about bookmark, say, how is the browser to know that it's going to have to start from the webapp's root url at https://mywebapp.com (this being the only real address that the web knows about?

None of this works in the webapps that you've seen so far in this post series. For example, while you can refresh the the display with any page content you choose, the address in the browser will remain unchanged - there is nothing to enable the user to create a bookmark that will reinstate that webapp view. Likewise, when you press the "back" button, all you will ever see is whatever application was using that browser tab before the webapp was launched.

In principle, none of this is a problem because javascript gives you ways of changing the tab address and manipulating tab history. But this is hard work and creates maintenance issues.

What is needed here is a standard library that delivers these arrangements without having to write them yourself. If you're already using React, the obvious thing will be to find a library that supplies this functionality. What you need, in short, is something like React-router.

React-router provides a simple way of writing a webapp that behaves as if it was made up of lots of separate physical pages whilst it's actually deployed as a single page.

But hang on - how about that little issue of arranging for https://mywebapp.com/about url calls to end up in https://mywebapp.com? The answer is that when you tell Firebase to configure your project as a "single-page" webapp, you are actually telling it to redirect all urls for that webapp to the root url for the site. You can see the configuration used to achieve this by looking at the firebase.json file in your project - you'll find it contains the following entry:

"rewrites": [
  {
    "source": "**",
    "destination": "/index.html"
  }
]
Enter fullscreen mode Exit fullscreen mode

This is telling whatever is deploying the webapp (ie Firebase or the React local server) to redirect all versions of the webapp's url to the index.html file at the project's root.

When the request arrives here, however, it reaches code that still sees the original bookmarked url that the user was trying to reach. Through the magic of React-router, the bookmarked url is then what gets rendered.

Let's have an example.

2. Introducing React-router

React-router is a library that you install with npm into a project that already contains React itself. Other router libraries exist, but React-router is currently (2023) probably the most popular.

The graphic below shows a typical menu layout for a React-router "MyApp" webapp. It is designed to offer users the option to select pages from a simple list of "tabs". When the user "mouses-over" a tab, the cursor's pointer changes to a "hand" symbol. If the user then clicks the mouse button, the webapp displays the associated page.

Simple menu layout

In this case, one tab on the menu displays a general description of the webapp, the other displays a list of its contributors. The plan is to create some code that uses React-router to deliver a working version of this.

To get going, you need to start by creating a new VSCode project, opening a terminal session, and installing React. But whereas previous posts in this series (see A student's guide to Firebase V9 - A very gentle introduction to React.js, have used create-react-app to build folder structure and install a local server, this post will switch to a newcomer called Vite. This offers significant time savings both when launching the local server and building a bundle ready for deployment. More significantly, however, Vite is now used in React Router's tutorial at The React-router tutorial. Since you're likely to want to refer to this, it now seems sensible to use Vite here too.

So, inside a VSCode terminal session in your new project, launch the following command:

npm create vite@latest . -- --template react
Enter fullscreen mode Exit fullscreen mode

and then

npm install react-router-dom
Enter fullscreen mode Exit fullscreen mode

followed by

npm run dev
Enter fullscreen mode Exit fullscreen mode

This should start a local server on your project and if you copy the local url it advertises, and paste this into your browser you should see a "Vite + React" version of the React logo you'll recall from earlier encounters with React.

So far so good then. The next stage is to overwrite the demo React Router code that is producing the logo with the code for this tutorial. If you look inside your project folder you'll see that things are a little bit different from the way that create-react-app would have "scaffolded" things.

For a start, all the files that you might have expected to have .js extensions are now .jsx. React-router is quite strict about requiring any javascript file that contains JSX code to signal this with a .jsx extension.

The project's index.html has also moved into the project root where it references a src/main.jsx script that in turn renders a src/App.jsx component. If you have a look at App.js, you'll see that this is a perfectly familiar component that, in this case is rendering the demo logo. So this is where you need to place the code for this tutorial.

Copy the following code and paste it, in its entirety, over the current content of App.js.

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

function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Tabs />} />
        <Route path="/about" element={<About />} />
        <Route path="/users" element={<Contributors />} />
      </Routes>
    </BrowserRouter >
  );
}

function Tabs() {

  return (
    <div>
      <ul>
        <li><Link to="/about">About</Link></li>
        <li><Link to="/users">Contributors</Link></li>
      </ul>
    </div>
  );
}

function About() {
  return (<h2>About</h2>);
}

function Contributors() {
  return (<h2>Contributors</h2>);
}

export default App;
Enter fullscreen mode Exit fullscreen mode

Save the file and the "React + Vite" display in your browser's local server tab should now have been replaced by the simple "MyApp" menu introduced earlier. Well, the formatting may look a bit odd (being centered vertically for a start) because the webapp is still subject to the demo code's index.cs, but that's irrelevant to this tutorial.

Note the url at this point - http://127.0.0.1:5174 (or whatever local port npm run dev has directed you to use ). The React server has read your amended App.jsx, compiled this into a bundle, and launched it into the browser. The javascript inside this in turn is now running and rendering your pages. Just now you're looking at the page rendered by the Tabs() component defined in the new version of App.jsx to display a menu of clickable links - "About" and "Contributors"

Click the "About" link. Ah - the Tabs() menu has disappeared and been replaced by an "About" page. Also, the url has changed to http://127.0.0.1:5174/about. Click the browser's "back" button and the menu tabs are restored. Click the "Contributors" tab and the "Contributors" page is displayed. Neat!

Now have a look at the code in src/App.js. What you have there is a functional component called App() that references three sub-components: Tabs, About, and Contributors. These are declared as functions directly within the body of src/App.js.

When you launch a React webapp, the first thing that runs is the src/index.js file created by Vite. If you look inside this, you'll see that it simply renders the current App() component. Initially, this would have been the demo React logo, but you've just overwritten it with the code you copied from the above. How does http://127.0.0.1:5174 end up displaying the "Tabs" component?

There are really just two major patterns in React-router code and both of these are clearly visible in src/App.js.

The first pattern is the nest of React-router <Route> tags laid out in the App() component itself. This is the core of React-router's page rendering action because this is what determines what gets displayed for a given url.

How will the webapp respond when it's called with its http://127.0.0.1:5174 root url?

As the <Routes> group renders its content for the initial cycle, each <Route> component in turn finds itself having to answer the same question: "does the 'path' bit of the current url match the path in your 'path=' prop?". Only if the answer is "yes" wil the <Route> component fire and render the component specified by its second, 'element=', prop.

In this instance, three <Route> within the <Routes> group are going to be asked this question in turn: Tabs, About, and Contributors. Where the current url is http://127.0.0.1:5174, only the Tabs component provides a match so this is the only component that gets rendered.

So far so good. What happens if you launch the webapp with http://127.0.0.1:5174/about ? Because mywebapp has been configured as a single-page webapp, the Firebase rewrites will redirect this call to http://127.0.0.1:5174. But this time, when App.js is called and the `` component is rendered, the path is now "about" and so it is the About component that renders.

The second important React-router pattern is the group of React-router <Link> tags in the Tabs component. The job of these components is to simulate tha action of the <a> anchor tags that you would have used in a conventional html design to code page links. You can't use actual <a> anchor tags here because these would want to take you to real url pages. In this case you want to use logical pages. The <Link> components deliver the fancy footwork to render the clicked component and also manipulate the url path displayed by the browser.

Obviously, there's a lot more to React-router than you've seen here, but I hope this post has at least explained the basics. One particularly popular requirement might be for a menu that "persists" as a header for every page. Have a look at 10.3 Reference - React-router project Templates, for a code template that does this. You'll also find some general remarks on project structure and naming conventions here.

Top comments (0)