Single Page Applications
A single page application (SPA) re-renders its content in response to navigation actions such as clicking a link, without making a request to the server and reloading the page.
How Does It Work?
In order to understand how single-page applications work, we first need to understand the default browser behavior and it's native APIs.
How Do Browsers Handle Navigation?
When you click a link, the browser changes the URL and triggers a request
to the endpoint specified in the link's href
attribute.
The browser then receives the response and creates the DOM, which is a document representing the page content. Each browser tab maintains a session history, which is an array of location entries, containing information about the location such its URL, state and the associated document.
As you're navigating through different pages, the browser is building up a session history. Each navigation makes a request to the server and creates a new entry.
That is how navigation is handled by the browser, but we need to prevent this default behavior for a single-page application to avoid requests hitting the server on every navigation action.
How do SPAs prevent this default behavior?
Location's hash #
Early single-page applications relied on the fact that you can change the URL's hash segment without triggering the browser to send a request to the server.
Home page:
https://example.com/
About page:
https://example.com/#/about
Now that we've prevented the browser from sending a request to the server
by changing the location's hash, we need to trigger a re-render and show
the correct content corresponding to the newly added hash (#/about
in the
example).
For that, single-page applications generally rely on a router. Routers are
made up of routes, which describe the location they should match. These
locations can be static paths like /about
or dynamic paths like
/products/:id
.
After matching a route, the router will trigger a re-render of the application to render the corresponding component, giving the user the impression of navigating to a different page.
While this worked, having to handle our routes using a hash string wasn't
ideal, until the History API
was developed to add first-class support for
single-page applications.
History API
The history object provides access to the browser's session history. It exposes useful methods and properties that let you navigate back and forth through the user's history, and manipulate the contents of the history stack.
Properties
length (read-only): returns an Integer representing the number of elements in the session history, including the currently loaded page.
scrollRestoration: allows setting default scroll restoration behavior on history navigation. This property can be either
auto
ormanual
.state (read-only): returns a value representing the state at the top of the history stack (the current entry). This is a way to look at the state without having to wait for a
popstate
event. The value is initiallynull
until thepushState
orreplaceState
methods are used.
Methods
back(): this asynchronous method goes to the previous page in the session history, the same action as when the user clicks the browser's Back button. Equivalent to
history.go(-1)
forward(): this asynchronous method goes to the next page in the session history, the same action as when the user clicks the browser's Forward button; this is equivalent to
history.go(1)
go(): asynchronously loads a page from the session history, identified by its relative location to the current page, for example, -1 for the previous page or 1 for the next page. Calling
go()
without parameters or a value of 0 reloads the current page.pushState(state, title, url): creates a new entry in the history stack using the current document together with the specified state, title and if provided, URL.
replaceState(state, title, url): updates the most recent entry on the history stack to have the specified state, title and if provided, URL.
Changing the State
pushState
and replaceState
methods have the same signature:
state: the state object is a JavaScript object which is associated with the entry. If you do not want to pass any state, pass
null
.title: or recently named
unused
exists for historical reasons and most browsers ignore this parameter. Passing an empty string here should be safe against future changes to the method.url [optional]: the new entry's URL is given by this parameter. Note that the browser won't attempt to load this URL after a call to
pushState
orreplaceState
. The new URL does not need to be absolute and if it's relative, it's resolved relative to the current URL. The new URL must be of the same origin as the current URL or it will throw an exception. If this parameter isn't specified, it's set to the document's current URL.
Navigating in SPAs using the History API
As explained above, we can use history.pushState
or
history.replaceState
to change the URL without sending a request to the
server. All that's left now, is to prevent the default browser behavior
when a link is clicked, which can be accomplished by adding an event
listener and using event.preventDefault
.
A simple implementation could look like this:
const Link = ({ children, href }) => (
<a
href={href}
onClick={event => {
event.preventDefault()
history.pushState(null, '', href)
}}
>
{children}
</a>
)
Now that we've changed the URL and prevented the browser from sending a request, all we need to do is to trigger a re-render of our application and show the relevant components matching the new URL. This is typically handled by the router module.
Fortunately router modules like React-Router merge all of these necessary steps and behaviors into custom components, hooks and APIs that can be used to handle client-side routing, freeing us from having to implement this from scratch.
Recap
That's it folks, we covered what a single-page application is, how it handles client-side routing and how it differs from traditional applications.
Next would be to understand how to serve single-page applications that use client-side routing. Things like static vs dynamic servers, static site generators, server-side route configuration and more. You can check the following note for further reading:
Resources
Here are some of the resources that inspired this note: