Svelte Single-Page Applications with Svelte Router SPA

Single-page applications (SPAs) provide user experiences that mirror those on desktop/mobile applications. Navigating from one page to the next no longer requires waiting for the browser to fully refresh the page on each transition, which keeps users engaged and productive. Because only a single page is served to a user, only certain sections of the view are dynamically updated by the browser (rather than the entire view via a full-page refresh) when the user navigates to a new page. To coordinate which sections of the view are dynamically updated for each route a user can navigate to, the single-page application must leverage a router to map these updates to each possible URL and render accordingly.

When a router is added to an application powered by a modern front-end library/framework, such as React.js and Svelte, developers are able to quickly build single-page applications due to the declarative APIs offered by these libraries/frameworks.

Compared to applications built with React.js, those built with Svelte are smaller (in bundle size) and faster. Svelte's compiler strips out the runtime overhead from outputted bundles and only ships the code that's needed. Additionally, Svelte does not rely on a virtual DOM and diffing algorithms (or other such techniques) for rendering updates. Instead, Svelte's compiler can determine ahead of time which parts of the application should change for a given state change, and when such state changes occur, it will surgically modify the DOM directly.

While not as large as the React.js ecosystem, the Svelte ecosystem continues to grow and gain traction. When considering all of the routers in the React.js ecosystem, there are two that are often chosen for their maturity, robustness and simple-to-use APIs: @reach/router and react-router.

To introduce the concept of integrating a router into a Svelte application, we will use an "easy to start with" router: svelte-router-spa.

Using this router and Svelte, we will be building the following dashboard as a single-page application:

Svelte Dashboard - Single-Page Application

Preparation#

To start, download this project from GitHub:

This repository was scaffolded from the sveltejs/template project template, and it will contain the all of the dashboard's components and views. We will integrate svelte-router-spa into this dashboard so it can...

  • Tie these components and views together.

  • Update certain content (in this case, the title bar and page view) as a user navigates from one page to the next.

    Title Bar + Page View

Installation#

Install svelte-router-spa as a dependency.

To run the application, execute the dev script:

Visit localhost:5000 in a browser. As you hover over each navigation bar item, notice that each item has an href value of /. Our objective will be to make these items link to their correct corresponding routes and render the correct page view.

To preview any of the other page views, go to the src/layouts/DashboardLayout.svelte and swap out the current <Home /> view with another view.

Example: Preview the <Projects /> view.

(src/layouts/DashboardLayout.svelte)

The browser should automatically refresh the application to reflect this change via hot reloading. Note that the title bar does not update to "Dashboard > Project" when the current view is <Projects />. This also needs to be fixed.

Dashboard Routes#

This dashboard will support the following routes:

Integrating the Router#

Inside of the src directory, add a routes.js file. Inside of this file, let's define the dashboard routes based on the information in the table above:

(src/routes.js)

Each object in routes represents an individual route. For each route, a URL pathname (name) is mapped to a Svelte component (directly via component or layout, or indirectly via redirectTo).

  • name - The URL pathname of a route.

  • redirectTo - A URL (or URL pathname) of a route to redirect to when visiting the route the redirectTo is defined within. In this case, visiting / will redirect the user to /dashboard, which will redirect the user to /dashboard/home.

  • component - A component that is rendered when the route is active.

  • layout - A component that is section of the application and is shared across multiple pages. It contains a child component defined within a nested route.

  • nestedRoutes - An array of routes with URL pathnames prefixed by a parent route's name. In this case, the component <Projects /> for nested route "projects" would be rendered when the user visits the route /dashboard/projects (/dashboard is the name of the nested route's parent). When the name of a nested route is set to "index," the redirect or rendering of a component (or layout) defined in this nested route will occur when its parent route is visited. In this case, visiting /dashboard will redirect the user to /dashboard/home.

Next, let's add the <Router /> component to src/App.svelte to indicate the top-most area in the application where updates will be made to the application when these routes are visited. Anything outside of <Router /> will not be affected. The layout (or component) that matches the current active route will be rendered inside of <Router />. In this case, when visiting /dashboard (or /dashboard/home, etc.), the <DashboardLayout /> will be rendered where <Router /> is placed.

(src/App.svelte)

Since <DashboardLayout /> is a layout component, it needs to define a place to render the child components defined in the nested routes of /dashboard. Let's add a <Route /> component to handle this. To render the correct child component for the current route, <Route /> must be passed information about the current route (and child routes) via the currentRoute object. This object is passed to the child components it renders.

(src/layouts/DashboardLayout.svelte)

In the browser, visit localhost:5000. Notice the redirection to localhost:5000/dashboard/home and the <Home /> view being rendered for this route. Now visit localhost:5000/dashboard/projects. The <Projects /> view is rendered for this route.

Fixing the Navigation Bar#

There are currently two issues with the navigation bar:

  1. The href value of each navigation bar item is incorrect. For example, when a user clicks on the "Projects" item, the browser's address bar should be updated to localhost:5000/dashboard/projects, and the browser should load the <Projects /> view.

  2. The navigation bar item that corresponds to the current active route is not highlighted. For example, if the current active route is localhost:5000/dashboard/home, then the colors of the text and background of the "Home" item should be changed to reflect that the user is currently visiting its route.

First, let's pass currentRoute to the <Navbar /> component in the <DashboardLayout /> layout component:

(src/layouts/DashboardLayout.svelte)

Next, let's propagate currentRoute to the <NavbarMenu /> component in the <Navbar /> component:

(src/components/Navbar.svelte)

With the removal of the dashboardRoutes prop, we will need to directly import the list of routes into the <NavbarMenu /> component to render the navigation items. Currently, routes is not formatted properly for this task. We will need to refactor the routes.js file to export a dashboardRoutes array, which will be used only to render these navigation items, and add a custom flag (isExcludedFromNav) to the "projects/:id" nested route object to tell <NavbarMenu /> to not render it as an item in the navigation bar.

(src/routes.js)

Additionally, we will need to pass the currentRoute object to the <NavbarMenuItem /> component.

(src/components/NavbarMenu.svelte)

Inside of the <NavbarMenuItem /> component, the isActive flag determines whether or not to highlight the navigation item when its corresponding route is the current active route.

svelte-router-spa provides a useful method for determining if a path is the current active route: routeIsActive. It accepts two arguments:

  • A pathname.

  • A boolean. If true, then consider the pathname to be active if it matches the current route exactly (not partially). If false, then vice-versa.

In this case, we would want the match to be exact.

The currentRoute has been passed to the <NavbarMenuItem /> component to trigger a reactive statement that will set the isActive flag whenever currentRoute changes. If the user clicks a navigation item, then the active status of the item should be changed to reflect the new current route. Hence, the following statement will correctly set the isActive flag whenever any of the navigation items is clicked:

Alternatively, you could substitute the <a /> element with the <Navigate /> component (a wrapper around an <a /> element) from svelte-router-spa to automatically add an active class if the route specified for to is the current route.

  • to - A valid, navigable route (similar to <a />'s href attribute).

  • styles - The classes to be applied to the underlying <a />'s class attribute.

  • title - The title to be applied to the underlying <a />'s title attribute.

All-in-all, the following changes to the <NavbarMenuItem /> component are the most comprehensive:

(src/components/NavbarMenuItem)

Verify that the navigation items work properly.

Navigation Bar Items

Verify that the active class automatically being set as a result of using the <Navigate /> component.

`active` Class

Extra Challenge: Make these same adjustments to the mobile navigation bar, which can be viewed when the browser width is less than 768px. These are the files that need to be modified: src/components/NavbarMobileMenu.svelte, src/components/NavbarMobileMenuItem.svelte and src/components/Navbar.svelte.

Syncing the Title Bar with the Current Route#

Notice that the title bar is not updated to reflect the current route as you navigate from one page to the next. Unfortunately, the currentRoute object does not provide a route label.

`currentRoute` Object

Let's export a new method, getRouteLabel from src/routes.js to retrieve the current route's label.

A namedParam is the segment of the URL pathname prefixed with a : in the route name. For example, id would be a namedParam in projects/:id.

routesMapping.withoutParams is an object that maps a route's pathname to its label. For example, /dashboard/projects is mapped to "Projects."

For routes without namedParams, getRouteLabel will check routesMapping.withoutParams for an exact pathname match and return a label for the matched pathname. For routes with namedParams, getRouteLabel will check an array of [ regular expression, label ] pairs and test the pathname against each pair's regular expression. The label of the first test that evaluates to true will be returned. If no matching routes are found, then an empty string is returned as the label.

Import getRouteLabel into the <TitleBar /> component, and set its value to currentRouteLabel via a reactive statement. This ensures the value of currentRouteLabel is updated whenever currentRoute has changed.

(src/components/TitleBar.svelte)

Verify that the title bar is updated whenever a navigation bar item is clicked.

Title Bar Verification

Update Label in Views with getRouteLabel#

Let's apply the same reactive statement to each view component to render the correct label in the placeholder content.

(src/views/Board.svelte, src/views/Calendar.svelte, ..., src/views/Timeline.svelte)