Building Modern SPAs with Vanilla JavaScript: A Beginner's Guide
Introduction Single Page Applications (SPAs) provide a smooth user experience by dynamically updating the page without requiring a full reload. Unlike traditional multi-page applications, where navigating between pages triggers full server requests, SPAs only update relevant sections. This results in faster load times and a more interactive experience. In this guide, we'll build a basic SPA using only HTML, CSS, and JavaScript—no frameworks required. What Makes a SPA Different? Before diving into the code, let's break down the key differences between SPAs and traditional web applications: Traditional Websites: Each navigation request loads a new HTML page from the server. SPAs: The entire application loads once, and JavaScript dynamically updates the content based on user interactions. Navigation in SPAs: URLs are managed using hash-based routing (#/home, #/about) or the History API. Performance Benefits: SPAs feel faster since only necessary data is fetched, rather than reloading entire pages. Now, let's get started by building our SPA step by step. 1. Setting Up the HTML Structure The HTML serves as the foundation of our SPA. We'll keep it simple: My SPA Home About Contact Explanation: The contains links for navigation. Clicking a link updates the URL hash (e.g., #/about). The serves as the main container for dynamic content. The JavaScript file (app.js) will handle routing and content updates. 2. Implementing a Simple Router Routing is how SPAs switch between different "pages" without reloading. We'll use JavaScript to listen for URL hash changes and update the content accordingly. Step 1: Define Page Content Functions Each function returns the content for a specific page. function getHomePage() { return `Welcome HomeThis is our SPA home page.`; } function getAboutPage() { return `About UsLearn about our company.`; } function getContactPage() { return `Contact UsGet in touch with our team.`; } Step 2: Set Up the Routing Table The routes object maps URL hashes to their respective functions. const routes = { '#/': getHomePage, '#/about': getAboutPage, '#/contact': getContactPage }; Step 3: Handle URL Changes The handleRoute function updates the #app div whenever the URL hash changes. function handleRoute() { const hash = window.location.hash || '#/'; // Default to home if no hash const content = routes[hash] ? routes[hash]() : 'Page Not Found'; document.getElementById('app').innerHTML = content; } // Listen for navigation events window.addEventListener('hashchange', handleRoute); window.addEventListener('load', handleRoute); 3. Managing State in the SPA State management keeps track of dynamic data in our app. Step 1: Define a State Object const state = { users: [], currentPage: 'home', isLoading: false }; Step 2: Update State and Refresh the View function updateState(newState) { Object.assign(state, newState); renderContent(); } Step 3: Render Content Based on State function renderContent() { const appDiv = document.getElementById('app'); if (state.isLoading) { appDiv.innerHTML = 'Loading...'; return; } appDiv.innerHTML = routes[window.location.hash || '#/'](); } 4. Handling User Interactions We'll use event delegation to handle user actions dynamically. Step 1: Add Event Listeners function setupEventListeners() { document.getElementById('app').addEventListener('click', function(event) { if (event.target.matches('.button-submit')) { handleSubmit(event); } if (event.target.matches('.toggle-menu')) { toggleMenu(event); } }); } Step 2: Implement Event Handlers function handleSubmit(event) { event.preventDefault(); updateState({ isLoading: true }); } function toggleMenu(event) { document.querySelector('.menu').classList.toggle('active'); } 5. Making the SPA Real-time with WebSockets WebSockets allow real-time updates, making our SPA dynamic. Step 1: Establish a WebSocket Connection const socket = new WebSocket('ws://your-server-url'); Step 2: Handle Incoming Messages socket.addEventListener('message', function(event) { const data = JSON.parse(event.data); updateState({ users: data.users, isLoading: false }); }); Step 3: Send Messages function sendMessage(message) { if (socket.readyState === WebSocket.OPEN) { socket.send(JSON.stringify(message)); } } Best Practices for SPAs Keep It Simple – Start small and test each feature independently. Organize Code Properly – Use modular functions and meaningful var

Introduction
Single Page Applications (SPAs) provide a smooth user experience by dynamically updating the page without requiring a full reload. Unlike traditional multi-page applications, where navigating between pages triggers full server requests, SPAs only update relevant sections. This results in faster load times and a more interactive experience. In this guide, we'll build a basic SPA using only HTML, CSS, and JavaScript—no frameworks required.
What Makes a SPA Different?
Before diving into the code, let's break down the key differences between SPAs and traditional web applications:
- Traditional Websites: Each navigation request loads a new HTML page from the server.
- SPAs: The entire application loads once, and JavaScript dynamically updates the content based on user interactions.
-
Navigation in SPAs: URLs are managed using hash-based routing (
#/home
,#/about
) or the History API. - Performance Benefits: SPAs feel faster since only necessary data is fetched, rather than reloading entire pages.
Now, let's get started by building our SPA step by step.
1. Setting Up the HTML Structure
The HTML serves as the foundation of our SPA. We'll keep it simple:
My SPA
href="#/">Home
href="#/about">About
href="#/contact">Contact
id="app">
Explanation:
- The
contains links for navigation. Clicking a link updates the URL hash (e.g.,
#/about
). - The serves as the main container for dynamic content.
- The JavaScript file (
app.js
) will handle routing and content updates.2. Implementing a Simple Router
Routing is how SPAs switch between different "pages" without reloading. We'll use JavaScript to listen for URL hash changes and update the content accordingly.
Step 1: Define Page Content Functions
Each function returns the content for a specific page.
function getHomePage() { return `
Welcome Home
This is our SPA home page.`
; } function getAboutPage() { return `About Us
Learn about our company.`
; } function getContactPage() { return `Contact Us
Get in touch with our team.`
; }Step 2: Set Up the Routing Table
The
routes
object maps URL hashes to their respective functions.
const routes = { '#/': getHomePage, '#/about': getAboutPage, '#/contact': getContactPage };
Step 3: Handle URL Changes
The
handleRoute
function updates the#app
div whenever the URL hash changes.
function handleRoute() { const hash = window.location.hash || '#/'; // Default to home if no hash const content = routes[hash] ? routes[hash]() : '
Page Not Found
'; document.getElementById('app').innerHTML = content; } // Listen for navigation events window.addEventListener('hashchange', handleRoute); window.addEventListener('load', handleRoute);3. Managing State in the SPA
State management keeps track of dynamic data in our app.
Step 1: Define a State Object
const state = { users: [], currentPage: 'home', isLoading: false };
Step 2: Update State and Refresh the View
function updateState(newState) { Object.assign(state, newState); renderContent(); }
Step 3: Render Content Based on State
function renderContent() { const appDiv = document.getElementById('app'); if (state.isLoading) { appDiv.innerHTML = '
Loading...'; return; } appDiv.innerHTML = routes[window.location.hash || '#/'](); }4. Handling User Interactions
We'll use event delegation to handle user actions dynamically.
Step 1: Add Event Listeners
function setupEventListeners() { document.getElementById('app').addEventListener('click', function(event) { if (event.target.matches('.button-submit')) { handleSubmit(event); } if (event.target.matches('.toggle-menu')) { toggleMenu(event); } }); }
Step 2: Implement Event Handlers
function handleSubmit(event) { event.preventDefault(); updateState({ isLoading: true }); } function toggleMenu(event) { document.querySelector('.menu').classList.toggle('active'); }
5. Making the SPA Real-time with WebSockets
WebSockets allow real-time updates, making our SPA dynamic.
Step 1: Establish a WebSocket Connection
const socket = new WebSocket('ws://your-server-url');
Step 2: Handle Incoming Messages
socket.addEventListener('message', function(event) { const data = JSON.parse(event.data); updateState({ users: data.users, isLoading: false }); });
Step 3: Send Messages
function sendMessage(message) { if (socket.readyState === WebSocket.OPEN) { socket.send(JSON.stringify(message)); } }
Best Practices for SPAs
- Keep It Simple – Start small and test each feature independently.
- Organize Code Properly – Use modular functions and meaningful variable names.
- Handle Errors Gracefully – Provide fallback UI elements.
- Optimize Performance – Reduce DOM updates and use event delegation.
Conclusion
By following this guide, you've built a fully functional SPA using vanilla JavaScript. This foundational knowledge will help you understand how frameworks like React and Vue work behind the scenes. Experiment by adding features such as local storage, animations, or API integrations to enhance your SPA.
Happy coding
- The JavaScript file (