Chapter 5: Applications

Applications: Composing components

Tomas Reimers
Tomas Reimers
Author

Once you've started to write large parts of your website with React, the natural extension becomes why not write the whole website in React?

Single-page apps (SPAs)

One of the most common way to write React apps are "single-page applications" (abbreviated SPAs, and pronounced S.P.A.s). In a SPA, the entire application is bundled into a single web-page.

History: Create React App

Although no longer officially supported (opens in a new tab) by the React documentation, the defacto way to create an SPA was Create React App (opens in a new tab) (abbreviated CRA, pronounced C.R.A): a scaffolder that sets up all the necessary build tooling for you (more on that in the next chapter), generates empty JS and CSS files for you to populate, and creates a single HTML file that looks something like this:

<html>
  <head>
    ... YOUR CSS AND JS FILES ...
  </head>
  <body>
    <div id="root" />
  </body>
</html>

The boilerplate JS finds #root and mounts your React into it, so all you have to do is write React. In addition, CRA installs a package react-scripts that had scripts to develop, build, and test CRA projects.

Like other scaffolders, than can be invoked directly from the command line without downloading anything:

npx create-react-app my-app

Vite

While CRA is deprecated, Vite (opens in a new tab) lives on:

npm create vite@latest

React router

While CRA only emits one HTML page, there are ways to make it seem as if your SPA has multiple pages.

The simplest solution is what's called "hash routing", where your application has many "pages" and the URL encodes them after the "hash" (which isn't sent up to the server, and doesn't create a reload):

your-single-page-app.com/#sign-in
your-single-page-app.com/#forgot-password
your-single-page-app.com/#user-account
...

Links (<a>) change the hash, and a framework can detect that and respond accordingly. One such framework is React Router (opens in a new tab), which acts as a switch statement mapping URLs to React components:

import * as React from "react";
import * as ReactDOM from "react-dom/client";
import {
  createBrowserRouter,
  RouterProvider,
} from "react-router-dom";
import "./index.css";
 
const router = createBrowserRouter([
  {
    path: "/",
    element: <div>Hello world!</div>,
  },
]);
 
ReactDOM.createRoot(document.getElementById("root")).render(
  <RouterProvider router={router} />
);

Beyond hash routing, your SPA can also have "real" (path-based) URLs:

your-single-page-app.com/sign-in
your-single-page-app.com/forgot-password
your-single-page-app.com/user-account
...

This is made possible by the history APIs (opens in a new tab) that allows you to change the URL without reloading the page. Links are replaced with custom <Link> elements that take the same props as <a> tags, but instead of actually causing the browser to navigate, they change the URL and update internal state of what page to show.

Deploying

Page-routing needs support from the server to work. For example, imagine I've navigated to "your-single-page-app.com/sign-in" and click refresh, the server returns a 404.

SPAs are typically deployed in an environment where you tell the server to route all-non-existing routes to "index.html", which, on load, reads the URL and shows the correct component.

A word of caution, an easy way to "hack" this behavior setting index.html to be a custom 404 page (a common configuration of what page to return when querying for paths that don't exist); however, this can harm your search engine ranking (frequently referred to as "search engine optimization", or SEO).

Server-side rendering (SSR)

While SPAs are easy, they have a handful of problems:

  • Because the initial load has nothing, your first render for a user will be slow (as the browser needs to download all the JavaScript, parse it all, and execute it all before anything shows up)
  • Because the initial load has nothing, many search engines won't index the contents of your page (Google has updated its crawler to run JavaScript and wait for the page to resolve, but not all of them have)
  • Because the initial load has nothing, metadata tags (including what image to show when your link is shared on social media (opens in a new tab)) won't be parsed by third-party services

A simple solution here was to begin server-side rendering webpages: rather than serving the empty HTML file, use a Nodejs server to do the initial render, and prepopulate the HTML. React then does its initial render on the client, reconciling against the HTML that already exists (a process called hydration (opens in a new tab)).

Static-site generation (SSG)

An alternative to SSR is to pre-render all of the pages at build time. While this doesn't work for pages that have user generated content or depend on the logged in user, it works great for blogs and documentation which know all of their content at build time.

Next.JS

Setting up SSR or SSG can be tricky, and these days the React docs recommend (opens in a new tab) using a framework to do that for you. Next.js (opens in a new tab) is a framework built by much of the original React team that optimizes not only site generation, but a whole lot of other things.

Much like Create React App, there is a Create Next App (opens in a new tab):

npx create-next-app@latest

Navigation

Out of the box, Next supports navigation. In Next there are two ways to define pages, the (now deprecated) pages router (opens in a new tab) and the newer app router (opens in a new tab). An example project might look like:

app/
  sign_in/
    page.js
  forgot_password/
    page.js
  account/
    settings/
      page.js

Each route is defined as folder paths within app/ and must terminate with a page.js. For example, this application would support the following URLs:

example.com/sign_in
example.com/forgot_password
example.com/account/settings

And, in turn, each page.js must export a React component that represents the page (opens in a new tab).

Much like React Router, Next provides a custom link that, on click, downloads the page and dynamically updates the DOM without triggering a browser refresh. (Something they confusingly name "client-side navigation" (opens in a new tab), not to be confused with "client-side routing", what React Router does.)

Server components / actions

Because every page is rendered on the server, Next introduces the concept of server components (opens in a new tab) and server actions (opens in a new tab) that allow you to fetch data on the server or talk directly to your database.

While it's possible to connect Next to your DB, many people choose not to use Next this way, and instead separate their API server from their Next server.

Bundle splitting

One of the main advantages of Next is that it breaks apart your JavaScript for you: because not every page needs every piece of JS, Next will automatically break apart your JS into the minimal amount needed for just the page: reducing page size and page load. Navigating to new pages will load additional JS.

Everything else

Next does a ton of other things for you: it optimizes images (opens in a new tab), preloads links (opens in a new tab), and supports SSR, SSG, and ISR (a hybrid approach) out of the box (along with so many other features and optimizations).

Next is configured through the next.config.js (opens in a new tab) file (included at the root of your repo).

Alternative frameworks

There are other ways to set up server-side- and statically- rendered projects, including Gatsby (opens in a new tab), Remix (opens in a new tab), and Astro (opens in a new tab) (Vite even has support for generating a server (opens in a new tab)).

Personally, I'm skeptical. For better, or for worse (opens in a new tab), Next.js now has a lot of the core React team working on it and is backed by a real company (Vercel (opens in a new tab)), which leads me to believe it will both shape the direction of React and outlive the alternatives. That said, who knows, it may not be winner take all.