Frontend Frameworks (React.js/Vue.js)

Chapter 6
Routing and Application Navigation

Bachelors Program in Full Stack Development
Frontend Frameworks (React.js/Vue.js)   •   Bachelor

Routing and Application Navigation

Introduction

A single-page application (SPA) loads an initial document and subsequently updates its interface through JavaScript rather than requesting a complete HTML document for every view. Routing gives an SPA a navigational structure: a URL is interpreted as application state, a route is selected, and the corresponding component is rendered. Effective routing therefore connects three concerns: browser history, user-interface composition, and access control.

React Router and Vue Router are the dominant routing solutions in their respective ecosystems. Although their APIs differ, both support declarative route definitions, nested views, dynamic parameters, navigation guards, redirects, lazy loading, error handling, and history integration. This chapter develops these capabilities and evaluates the design decisions behind them.

Learning Objectives

By the end of this chapter, you should be able to:

  • explain how client-side routing differs from traditional server navigation;
  • configure React Router and Vue Router for browser-history navigation;
  • design nested, dynamic, wildcard, and error routes;
  • distinguish route parameters, query parameters, and navigation state;
  • implement layouts, protected routes, authentication redirects, and guards;
  • apply lazy loading and route-level error handling;
  • analyze browser refresh, back-button, accessibility, security, and deployment issues.

1. The Routing Model

A route is a mapping between a URL pattern and a view. For example, /courses/42 may identify the course whose identifier is 42. A router generally performs the following sequence:

  1. observes a URL change;
  2. matches the URL against route definitions;
  3. extracts parameters and metadata;
  4. renders the matched component tree;
  5. updates browser history when navigation was initiated by the application.

In a traditional multi-page application, the browser requests a new document from a server. In an SPA, a client-side router intercepts internal links, changes the history entry, and renders a new component without a full-page reload. The server must still be configured to return the SPA entry document for valid client-side paths.

URL components

Consider:

https://example.edu/courses/42?tab=outline#assessment

ComponentMeaningTypical use
Path/courses/42Resource and hierarchy
Route parameter42Identity of a resource
Query string?tab=outlineFiltering, sorting, view options
Hash fragment#assessmentPosition within a document
Originhttps://example.eduProtocol, host, and port

A route parameter is normally required to match a route, whereas a query parameter is optional and interpreted by application logic. Sensitive information should not be placed in either location merely because the URL is convenient: URLs can be stored in history, logs, browser synchronization, and analytics systems.

2. React Router

Modern React Router commonly uses createBrowserRouter and RouterProvider, although the component-based BrowserRouter, Routes, and Route API remains widely used. The data-router form makes loaders, actions, and route-level error elements available.

Basic configuration

import { createBrowserRouter, RouterProvider } from "react-router-dom";
import Home from "./Home";
import About from "./About";

const router = createBrowserRouter([
  { path: "/", element: <Home /> },
  { path: "/about", element: <About /> }
]);

export default function App() {
  return <RouterProvider router={router} />;
}

Use Link or NavLink for internal navigation. An ordinary anchor causes a document request and should be reserved for external destinations or deliberate reloads.

import { NavLink } from "react-router-dom";

export function Navigation() {
  return (
    <nav aria-label="Primary navigation">
      <NavLink to="/">Home</NavLink>
      <NavLink to="/about">About</NavLink>
    </nav>
  );
}

NavLink can apply an active style, which helps users understand their current location. Navigation should also have meaningful link text and keyboard access.

Nested routes and layouts

Nested routes represent a hierarchy in both URL structure and interface structure. An outlet is the insertion point at which a child route renders.

import { Outlet } from "react-router-dom";

function DashboardLayout() {
  return (
    <div className="dashboard">
      <aside>{/* dashboard navigation */}</aside>
      <main><Outlet /></main>
    </div>
  );
}

const router = createBrowserRouter([
  {
    path: "/dashboard",
    element: <DashboardLayout />,
    children: [
      { index: true, element: <Overview /> },
      { path: "reports", element: <Reports /> },
      { path: "settings", element: <Settings /> }
    ]
  }
]);

The index route is the default child for /dashboard. Child paths are relative: reports produces /dashboard/reports, not a separate top-level path. A layout should contain stable structure, while the outlet contains the changing view.

Dynamic routes and route data

const router = createBrowserRouter([
  { path: "/courses/:courseId", element: <Course /> }
]);

function Course() {
  const { courseId } = useParams();
  // Validate courseId before requesting data.
  return <h1>Course {courseId}</h1>;
}

A parameter is a string by default. If an API requires an integer, validate and convert it explicitly, and handle invalid values. In data routers, a loader can retrieve data before rendering:

async function courseLoader({ params }) {
  const response = await fetch(`/api/courses/${encodeURIComponent(params.courseId)}`);
  if (response.status === 404) throw new Response("Not found", { status: 404 });
  if (!response.ok) throw new Error("Course request failed");
  return response.json();
}

The component can read the result using useLoaderData. Production code should also address cancellation, authorization, and stale data.

Query parameters and navigation state

React Router's useSearchParams is appropriate for shareable state such as a filter:

const [searchParams, setSearchParams] = useSearchParams();
const sort = searchParams.get("sort") || "name";

function changeSort(value) {
  setSearchParams({ sort: value });
}

Location state is temporary, history-associated state and is useful for a return path or display context:

navigate("/login", { state: { from: location.pathname } });

It is not a reliable database, authentication mechanism, or cross-device persistence layer. A refresh or direct visit may not contain the expected state.

3. Vue Router

Vue Router 4 is designed for Vue 3. A router is created, registered with the application, and rendered through RouterView.

// router/index.js
import { createRouter, createWebHistory } from "vue-router";
import HomeView from "../views/HomeView.vue";
import AboutView from "../views/AboutView.vue";

const routes = [
  { path: "/", name: "home", component: HomeView },
  { path: "/about", name: "about", component: AboutView }
];

export const router = createRouter({
  history: createWebHistory(),
  routes
});
// main.js
import { createApp } from "vue";
import App from "./App.vue";
import { router } from "./router";

createApp(App).use(router).mount("#app");

The root component generally includes <RouterView />. Internal links use <RouterLink>, which avoids full document reloads.

Nested routes and named navigation

const routes = [
  {
    path: "/dashboard",
    component: () => import("../layouts/DashboardLayout.vue"),
    children: [
      { path: "", name: "dashboard", component: () => import("../views/Overview.vue") },
      { path: "reports", name: "reports", component: () => import("../views/Reports.vue") }
    ]
  }
];

The parent component must render <RouterView /> for its children to appear. Named routes reduce dependence on hard-coded URL strings:

router.push({ name: "reports" });
router.push({ name: "course", params: { courseId: "42" } });

Parameters, queries, and composition API

const routes = [
  { path: "/courses/:courseId", name: "course", component: CourseView }
];

In a component:

import { useRoute, useRouter } from "vue-router";

const route = useRoute();
const router = useRouter();
const courseId = route.params.courseId;
const tab = route.query.tab || "overview";
router.push({ name: "course", params: { courseId }, query: { tab: "outline" } });

Route objects are reactive, but destructuring values carelessly can remove reactivity. Use computed properties or watch the relevant route fields when data must reload after a parameter changes.

4. Guards and Authentication Flows

A navigation guard is a policy checkpoint. It may allow navigation, redirect, or cancel it. Guards improve user experience but do not replace server-side authorization. An attacker can directly call an API regardless of a hidden client route.

React protected routes

A small wrapper can handle presentation-level protection:

function RequireAuth({ children }) {
  const user = useAuth();
  const location = useLocation();

  if (user === undefined) return <Loading />;
  if (!user) return <Navigate to="/login" replace state={{ from: location }} />;
  return children;
}

The loading state is important. Treating an initially unknown user as logged out can cause a redirect flash. After login, validate the stored return location and permit only safe internal paths; never blindly redirect to an arbitrary URL supplied by a query string.

Vue global guard

router.beforeEach((to) => {
  const auth = useAuthStore();

  if (to.meta.requiresAuth && !auth.isAuthenticated) {
    return {
      name: "login",
      query: { redirect: to.fullPath }
    };
  }

  if (to.meta.roles && !to.meta.roles.includes(auth.user?.role)) {
    return { name: "forbidden" };
  }
});

The route metadata might be:

{
  path: "/admin",
  component: AdminView,
  meta: { requiresAuth: true, roles: ["admin"] }
}

A guard should be deterministic, short, and free from unnecessary network requests. If it must verify a session, use a centralized authentication service and avoid redirect loops. A forbidden route (authenticated but insufficiently authorized) should be distinguished from a login route (not authenticated).

5. Lazy Loading and Performance

Route-level code splitting loads a view only when the user visits it. React uses dynamic imports with lazy and Suspense:

const Reports = lazy(() => import("./Reports"));

<Suspense fallback={<Loading />}> 
  <Reports />
</Suspense>

Vue Router accepts an asynchronous component factory:

{ path: "/reports", component: () => import("../views/Reports.vue") }

Lazy loading reduces the initial JavaScript payload but creates a later network request. Provide loading feedback, handle chunk-load failures, and avoid splitting every tiny component. Split at meaningful route or feature boundaries. Prefetching may improve perceived performance, but excessive prefetching consumes bandwidth and can disadvantage mobile users.

6. Error Routes and Failure Handling

A robust router distinguishes at least three failures:

  • No match: the URL does not correspond to a route;
  • Authorization failure: the route exists but the user lacks access;
  • Data or component failure: a request or lazy chunk fails.

React Router can define a wildcard route and an errorElement:

{
  path: "*",
  element: <NotFound />
}

An error boundary or route error element should provide recovery actions such as returning home, retrying, or signing in. Do not expose stack traces, tokens, or internal API details to users.

Vue Router uses a catch-all parameter:

{ path: "/:pathMatch(.*)*", name: "not-found", component: NotFoundView }

Error pages should preserve the global layout where appropriate, set a meaningful document title, and communicate status to assistive technology. A not-found page is not merely cosmetic: correct HTTP status handling may still be required by the server for SEO and monitoring.

7. Browser History and Deployment

The browser history stack records URL entries. push creates a new entry; replace changes the current entry. Use replace after login redirects or canonicalization when returning to the previous page would create an undesirable loop. Use push for ordinary user choices that should be revisitable with the Back button.

createBrowserHistory and createWebHistory produce clean paths such as /courses/42, but the web server must route unknown application paths to the SPA entry file. Without this fallback, navigation from within the application works while refreshing /courses/42 returns a server 404.

Hash history, such as /#/courses/42, avoids some server fallback configuration because the fragment is not sent to the server. It can be useful on constrained static hosting, but it produces less conventional URLs and may interact differently with analytics and accessibility tools.

A deployment checklist includes:

  • fallback configuration for client routes;
  • correct asset base paths under subdirectories;
  • server handling for genuine API and static-file 404 responses;
  • cache policies that do not leave an obsolete entry bundle indefinitely;
  • HTTPS and secure cookie configuration;
  • testing of direct loads, refreshes, back/forward navigation, and deep links.

8. A Comparative Design View

ConcernReact RouterVue Router
Root integrationRouterProvider or BrowserRouterapp.use(router)
View outletOutletRouterView
Internal linkLink, NavLinkRouterLink
ParametersuseParamsuseRoute().params
Programmatic navigationuseNavigateuseRouter().push
Guardswrappers, loaders, navigation logicbeforeEach, beforeEnter, component guards
Lazy routelazy or dynamic importdynamic import factory
Not-found routepath: "*"/:pathMatch(.)

The transferable principle is not the API spelling. It is the separation of route declaration, view composition, navigation policy, data loading, and error recovery.

9. Illustrative Case: University Course Portal

Background

A university builds a course portal with public course discovery, authenticated student dashboards, instructor administration, and a results page. Students often enter through links in email, refresh deep URLs, and use browser Back to compare courses.

Problem

An initial prototype used conditional rendering based on a global currentPage variable. It could not represent deep links, lost state on refresh, and displayed dashboard content briefly before authentication completed.

Analysis

The portal requires public routes (/, /courses, /courses/:id), protected student routes (/dashboard, /dashboard/results), and role-based instructor routes (/instructor). Course filters should be shareable, so they belong in query parameters. A temporary “returned from login” context may be kept in navigation state, but the authenticated session must be established by a secure server-backed mechanism.

Approach

The team introduced a browser-history router and a layout hierarchy. The course detail page uses a dynamic parameter and a loader or composable to fetch the course. Dashboard children render inside a persistent dashboard layout. Authentication begins with a guard that waits for session initialization; unauthenticated users are redirected to login with a validated internal return path. Instructors receive a forbidden page rather than a misleading not-found response. Reports and instructor tools are lazy-loaded.

The server was configured to return the SPA entry file for application paths while retaining proper API responses. Automated tests covered direct navigation, refresh, unauthorized access, role changes, malformed identifiers, chunk failure, and keyboard navigation.

Outcome

Students could bookmark and share course pages, the Back button behaved predictably, and the initial bundle became smaller because rarely used tools were split. The system still enforced authorization on every protected API endpoint, preventing the router from becoming a false security boundary.

Learning points

  1. URL design is part of application architecture, not merely a presentation detail.
  2. Layout nesting should reflect stable interface structure.
  3. Authentication state needs an explicit loading phase.
  4. Client guards improve navigation but cannot authorize server operations.
  5. Deployment configuration is part of routing correctness.

10. Accessibility, Security, and Professional Practice

Accessible navigation requires semantic landmarks, visible focus, descriptive page titles, keyboard-operable controls, and an announced route change where appropriate. Focus management deserves deliberate design: after navigation, focus may move to the page heading or main region, but it should not be moved in a way that disorients users. Avoid replacing links with click-only elements.

Security practice includes validating route parameters, encoding values used in requests, avoiding open redirects, keeping tokens out of URLs, and enforcing authorization server-side. Route metadata must not be treated as proof of identity. A client bundle is inspectable by users, so hiding an administrative link does not protect an administrative operation.

Privacy considerations include analytics on route changes, referrer leakage, and sensitive query strings. Minimize personally identifiable information in URLs and define retention practices for navigation telemetry.

11. Best Practices and Limitations

Use stable, meaningful route names and paths; document whether identifiers are case-sensitive; keep route components focused; centralize authentication state; show loading and error states; use replace intentionally; and test deep links in the deployed environment. Prefer query parameters for reproducible filters and component state for transient presentation details.

Routing also has limitations. Large route trees can become difficult to reason about. Guards may cause waterfalls if they fetch too much data. Client-side navigation does not automatically improve performance, and lazy loading can increase interaction latency. Browser history is not a substitute for application persistence. Finally, router APIs evolve, so teams should consult the version-specific documentation and test migration behavior rather than copying examples from incompatible releases.

Chapter Summary

Client-side routing maps URLs to component trees while preserving the SPA experience. React Router uses concepts such as routes, outlets, loaders, navigation hooks, and error elements; Vue Router uses route records, RouterView, named navigation, reactive route objects, and guards. Nested routes express layouts, dynamic parameters identify resources, query strings represent shareable options, and navigation state carries limited transient context. Authentication flows should distinguish unknown, unauthenticated, and unauthorized states, while server-side authorization remains mandatory. Lazy loading improves initial payload size when applied at sensible feature boundaries. Wildcard routes, error boundaries, deployment fallbacks, accessible navigation, secure redirect handling, and browser-history testing are essential to a production-quality application.

Key Terms

Route: A rule mapping a URL pattern to application behavior or a view.

Single-page application: An application that updates views without fully reloading the document for each internal navigation.

Router: Software that matches URLs and coordinates navigation and rendering.

Route parameter: A dynamic path segment such as :courseId.

Query parameter: A key-value value in the URL query string, often used for filters or options.

Navigation state: Temporary state attached to a history navigation.

Nested route: A route rendered within the outlet or view of a parent route.

Layout: A stable interface shell containing an insertion point for changing views.

Outlet: React Router’s rendering position for a matched child route.

RouterView: Vue Router’s rendering position for a matched route component.

Navigation guard: Logic that permits, redirects, or cancels navigation.

Lazy loading: Loading code only when a feature or route is needed.

Code splitting: Dividing an application bundle into separately loadable chunks.

Wildcard route: A fallback route used when no more specific route matches.

Browser history: The user agent’s stack of navigable URL entries.

Push navigation: Navigation that creates a new history entry.

Replace navigation: Navigation that changes the current history entry.

Authentication: Establishing or verifying a user’s identity.

Authorization: Determining whether an identified user may perform an action.

Open redirect: A vulnerability in which an application redirects users to an attacker-controlled destination.

Review Questions

Short Answer Questions

  1. What is the difference between a route parameter and a query parameter?
  2. Why must an SPA server be configured to support deep links when browser history is used?
  3. What is the purpose of an outlet in React Router or a RouterView in Vue Router?
  4. Why should authentication initialization expose a loading state?
  5. What is the difference between push and replace navigation?

Long Answer Questions

  1. Compare the route configuration and nested-layout mechanisms of React Router and Vue Router.
  2. Explain how a protected route should handle unauthenticated, authenticated, and unauthorized users.
  3. Discuss the performance advantages and limitations of route-level lazy loading.
  4. Design a routing structure for a university portal containing public, student, and instructor areas.
  5. Explain why client-side route guards cannot replace server-side authorization.

Analytical / Application Questions

  1. A user opens /dashboard/results directly after clearing cookies. Analyze the navigation states that should occur and propose a robust redirect flow.
  2. A Vue application works when links are clicked but returns a server 404 after refreshing /courses/42. Identify the cause and specify deployment remedies.
  3. A React application stores a payment-card number in a query parameter so that a checkout page can restore it after login. Critically evaluate this design and propose a safer alternative.

Multiple Choice Questions with Answers

Question 1

Which component renders a matched child route in React Router?

A. RouterView

B. Outlet

C. RouteLink

D. HistoryProvider

Correct Answer: B. Outlet

Question 2

Which URL element is most appropriate for a shareable course filter such as sorting by title?

A. A query parameter

B. A password cookie

C. A component-local variable only

D. A server stack trace

Correct Answer: A. A query parameter

Question 3

What is the principal purpose of a navigation guard?

A. To compress JavaScript bundles

B. To replace all API authorization

C. To allow, redirect, or cancel navigation

D. To create database indexes

Correct Answer: C. To allow, redirect, or cancel navigation

Question 4

Which Vue Router syntax represents a catch-all route in Vue Router 4?

A. path: "*"

B. path: "/all"

C. path: "/:pathMatch(.)"

D. path: "/404-only"

Correct Answer: C. path: "/:pathMatch(.)"

Question 5

Why is a loading state needed while authentication is being initialized?

A. It prevents all route components from compiling

B. It avoids treating an unknown session as definitely logged out

C. It changes query parameters into route parameters

D. It disables browser history

Correct Answer: B. It avoids treating an unknown session as definitely logged out

Question 6

What is a likely result of using replace navigation after a login redirect?

A. The login page is always opened in a new tab

B. The current history entry is changed rather than adding another one

C. The server receives two API requests

D. All lazy-loaded chunks are prefetched

Correct Answer: B. The current history entry is changed rather than adding another one

Question 7

Which statement about a client-side route guard is correct?

A. It alone protects an API from unauthorized requests

B. It is enforced by every browser before JavaScript runs

C. It improves navigation policy but must be backed by server authorization

D. It permanently encrypts route parameters

Correct Answer: C. It improves navigation policy but must be backed by server authorization

Question 8

What is a principal benefit of route-level code splitting?

A. It guarantees zero network requests after startup

B. It can reduce the initial JavaScript payload

C. It removes the need for error handling

D. It converts browser history to hash history

Correct Answer: B. It can reduce the initial JavaScript payload

Question 9

Which element should normally be used for internal SPA navigation?

A. A router link component

B. An image without alternative text

C. A database trigger

D. A CSS pseudo-element

Correct Answer: A. A router link component

Question 10

A user is authenticated but lacks the required administrator role. Which outcome best represents this state?

A. Redirect to a forbidden or insufficient-permission page

B. Treat the user as unauthenticated and delete the session

C. Expose the administrator component anyway

D. Convert the route into a query parameter

Correct Answer: A. Redirect to a forbidden or insufficient-permission page

Frontend Frameworks (React.js/Vue.js)
Chapter 6 of 14