What Are SPAs — Single-Page Applications Explained for Beginners
In this tutorial, you will learn about What Are SPAs. We cover key concepts, practical examples, and best practices to help you master this topic.
Single-Page Applications load a single HTML page and dynamically update content via JavaScript, providing smooth app-like experiences without full page reloads unlike traditional multi-page websites.
What You'll Learn
By the end of this tutorial, you will understand what SPAs are, how they work under the hood, their benefits and drawbacks, and when to use them.
Why It Matters
SPAs power most modern web applications — Gmail, Google Maps, Facebook, Twitter, and Trello all use SPA architecture. Understanding SPAs is essential for building modern interactive web applications that users expect to feel instant and responsive.
Real-World Use
Gmail loads a single HTML page and then fetches emails via AJAX as you navigate. Clicking a label or email does not reload the page — the UI updates instantly. This makes Gmail feel like a desktop application rather than a website.
How SPAs Work
SPA Architecture
Initial Load: Server sends HTML + CSS + JS bundle
↓
Browser renders page and executes JavaScript
↓
JavaScript initializes the app
(router, state management, components)
↓
┌──────────────────────────────────────────┐
│ User Interaction │
│ Click link → Router intercepts │
│ → Updates URL (pushState) │
│ → Fetches data (fetch/XHR) │
│ → Renders new component │
│ → Updates DOM without reload │
└──────────────────────────────────────────┘
↓
NO full page reloads during use
Think of an SPA like a well-organized desk. The initial load is like setting up your desk with all the tools you might need (pens, paper, computer). Once set up, you do not have to rebuild the desk every time you switch tasks — you just pick up a different tool (component) and keep working.
Basic SPA Architecture
// Minimal SPA router
const routes = {
'/': { title: 'Home', render: () => '<h1>Home</h1><p>Welcome to the SPA</p>' },
'/about': { title: 'About', render: () => '<h1>About</h1><p>About this application</p>' },
'/contact': { title: 'Contact', render: () => '<h1>Contact</h1><p>Contact us here</p>' }
};
function navigate(path) {
// Update browser URL without page reload
history.pushState(null, '', path);
// Update page title
document.title = routes[path]?.title || 'SPA';
// Render new content
const app = document.getElementById('app');
app.innerHTML = routes[path]?.render() || '<h1>404 Not Found</h1>';
}
// Intercept link clicks
document.addEventListener('click', (event) => {
const link = event.target.closest('a');
if (link && link.href.startsWith(window.location.origin)) {
event.preventDefault();
navigate(new URL(link.href).pathname);
}
});
// Handle browser back/forward
window.addEventListener('popstate', () => {
navigate(window.location.pathname);
});
// Initial render
navigate(window.location.pathname);
Output:
URL: https://example.com/about
Page title: About
Content displayed: About this application
No page reload occurred
SPA vs MPA Comparison
// SPA: Single HTML file, dynamic content
// MPA: Multiple HTML files, server-rendered
// SPA navigation flow
navigator.serviceWorker?.controller?.postMessage('hello');
fetch('/api/data').then(data => renderUI(data));
history.pushState(null, '', '/new-page');
// MPA navigation flow
// Browser loads entirely new HTML document from server
// <a href="/new-page"> → full page reload → new HTML
Client-Side Routing
SPAs use two routing approaches:
History API: Clean URLs like /users/123 using pushState() and popstate. Requires server configuration to serve the SPA for all routes.
Hash Routing: URLs like /#/users/123 using the hash fragment. Does not require server configuration but produces less clean URLs.
// History API routing
history.pushState({ userId: 123 }, '', '/users/123');
window.addEventListener('popstate', (event) => {
console.log('Navigated to:', event.state);
renderUser(event.state.userId);
});
// Hash routing
window.addEventListener('hashchange', () => {
const path = window.location.hash.slice(1) || '/';
console.log('Hash route:', path);
renderRoute(path);
});
Data Fetching
SPAs fetch data asynchronously after the initial page load:
async function loadUserData(userId) {
try {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) throw new Error('Failed to load user');
const user = await response.json();
renderUserProfile(user);
} catch (error) {
renderError(`Could not load user: ${error.message}`);
}
}
function renderUserProfile(user) {
document.getElementById('app').innerHTML = `
<h1>${user.name}</h1>
<p>Email: ${user.email}</p>
<p>Role: ${user.role}</p>
<button onclick="loadUserData(${user.id + 1})">Next User</button>
`;
}
Common Mistakes
- Forgetting server-side route fallback. When using History API routing, configure your server to serve index.html for all routes. Otherwise, refreshing a deep link returns 404.
- Not handling loading states. API calls take time. Always show loading indicators during data fetching to prevent blank screens.
- Memory leaks from event listeners. SPAs run for a long time. Unused event listeners accumulate. Clean up with removeEventListener.
- Ignoring the back button. Users expect browser back/forward to work. Always handle the popstate event.
- Large initial bundle. Loading all JavaScript upfront defeats the purpose of SPAs. Implement Code Splitting to load routes on demand.
Practice Questions
- What is the fundamental difference between an SPA and an MPA?
- How does client-side routing work in SPAs?
- What are the two main approaches to client-side routing?
- Why must you configure server fallback for History API routing?
- What problem does code splitting solve in SPAs?
Challenge: Build a minimal SPA with three routes (home, about, contact) using the History API. Implement click interception on links, popstate handling for back/forward, and a loading state during simulated data fetch.
FAQ
Mini Project
Build a simple SPA with a todo list: three routes (home showing stats, todos showing the list, about), client-side routing with History API, data fetching from a mock API (localStorage), and basic state management using a global store object.
What's Next
You understand what SPAs are. Next, compare SPA vs MPA in depth to understand when each architecture is the right choice.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro