Vanilla Spas
Master single-page application architecture using pure JavaScript — routing, views, state management, and professional patterns.
Part of the free JavaScript course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
What is a Single Page Application (SPA)?
A Single Page Application is a web app where the page loads once and navigation happens client-side. New content is injected into the page dynamically without browser reloads.
Traditional Multi-Page
- • Click link → server sends new HTML
- • Whole screen reloads
- • State/UI resets on navigation
- • Slower perceived performance
SPA Approach
- • One HTML file loads once
- • JavaScript injects content
- • No flicker, no reload
- • App-like experience
💡 Real-World SPAs: YouTube, Instagram, Twitter, and most dashboard apps use SPA architecture. Building SPAs with pure JavaScript helps you understand how frameworks like React and Vue work internally.
Core SPA Principles
To build your own SPA, you need these components:
1. Routing System
Determines which page/view to show based on URL
2. Views/Templates
3. Rendering Logic
4. History Management
5. State Handling
6. Event Delegation
Two Main Routing Strategies
1. Hash-based Routing (#home, #about)
Uses the URL hash (#). Doesn't reload the page and works everywhere. Easiest to implement.
2. History API Routing (/home, /about)
Clean URLs without hashes. Uses pushState() and popstate. Requires server configuration.
💡 Which to Choose: For beginners, use hash routing. For production apps, History API gives cleaner URLs but requires server rewrites to serve index.html for all routes.
Basic SPA Folder Structure
Defining Views (Each Page)
Each page is a function that returns HTML (as a string or DOM elements).
Building a Simple Hash Router
Create a router that loads views based on the URL hash:
Navigation Links
Event Delegation (Critical for SPAs)
Since the DOM is replaced on navigation, you can't attach events directly to elements that might disappear.
💡 Why This Matters: Event delegation is essential for SPAs because the DOM changes frequently. By listening on a parent element (like document), your handlers automatically work for dynamically inserted elements.
State Management Basics
SPAs need to track application state (user data, UI state, etc.) and re-render when it changes.
Pub/Sub State Store (Mini Redux)
Route Parameters (#/user/123)
Real apps need dynamic routes. Here's how to add parameter support:
Lazy Loading Routes (Dynamic Imports)
Load pages only when needed for massive performance gains:
💡 Why Lazy Load: With lazy loading, users only download the JavaScript they need. A 500KB app might only load 50KB initially, making the first page load much faster.
Navigation Guards (Protected Routes)
Control access to routes based on authentication or other conditions:
Component Architecture
Structure your UI with reusable components, similar to React:
Full Router Class (Framework-Grade)
Build a reusable router class like React Router or Vue Router:
API Loading & Error States
Professional SPAs show loading states and handle errors gracefully:
View Transitions (Smooth Navigation)
Add CSS transitions between views for a polished feel:
SPA Performance Optimization
Do ✅
- • Lazy load routes/views
- • Use event delegation
- • Cache API responses
- • Use CSS transitions
- • Batch DOM updates
- • Preload likely routes
Avoid ❌
- • Deep nested DOM updates
- • Re-rendering entire app
- • Attaching events to dynamic elements
- • Loading all JS upfront
- • Storing state in DOM
- • Blocking main thread
SPA Security (XSS Prevention)
SPAs must defend against XSS and other injection attacks:
💡 Security Rule: Never trust user input. Always escape or sanitize any data that comes from users, URLs, or APIs before inserting into the DOM.
SPA Deployment
For History API routing, configure your server to serve index.html for all routes:
Complete Mini SPA Example
Here's a complete working SPA in one file to get you started:
💡 Try It: Copy this HTML into a file and open it in your browser. Click the navigation links and try the counter — all without page reloads!
Key Takeaways
- ✅ SPAs load once and handle navigation with JavaScript
- ✅ Hash routing is easiest; History API gives cleaner URLs
- ✅ Views are functions that return HTML strings
- ✅ Event delegation is essential for dynamic DOM
- ✅ Use a simple state object and re-render on changes
- ✅ Lazy loading improves initial page load
- ✅ Navigation guards protect private routes
- ✅ Always escape user input to prevent XSS
- ✅ Building vanilla SPAs teaches you how frameworks work internally
Practice quiz
What defines a Single Page Application (SPA)?
- It has only one HTML element
- It cannot use JavaScript
- The page loads once and navigation happens client-side without full reloads
- It always needs a database
Answer: The page loads once and navigation happens client-side without full reloads. In an SPA the page loads once and navigation injects new content client-side without browser reloads.
Which routing strategy is easiest and works everywhere with no server config?
- Hash-based routing (#home, #about)
- History API routing
- Server-side routing
- DNS routing
Answer: Hash-based routing (#home, #about). Hash-based routing uses the URL hash, requires no server configuration, and works everywhere.
Which browser event fires when the URL hash changes?
- popstate
- load
- click
- hashchange
Answer: hashchange. The 'hashchange' event fires when the URL hash changes, letting the router respond.
What does the History API use to change the URL without reloading?
- location.reload()
- history.pushState()
- document.write()
- window.open()
Answer: history.pushState(). History API routing uses history.pushState() to change the URL and listens to popstate for back/forward.
Why does History API routing require server configuration?
- The server must serve index.html for all routes so deep links and refreshes work
- It needs a database
- It uses WebSockets
- It needs HTTPS only
Answer: The server must serve index.html for all routes so deep links and refreshes work. Clean URLs require server rewrites that return index.html for every route, or refreshes would 404.
In the lesson, how is each view typically represented?
- As a CSS class
- As a database row
- As a function that returns HTML (a string or DOM)
- As an image
Answer: As a function that returns HTML (a string or DOM). Each page/view is a function that returns HTML, which the router injects into the app container.
Why is event delegation essential in SPAs?
- It makes CSS faster
- Because the DOM is replaced on navigation, listening on a parent (like document) keeps handlers working for dynamic elements
- It encrypts events
- It prevents reloads
Answer: Because the DOM is replaced on navigation, listening on a parent (like document) keeps handlers working for dynamic elements. Since views replace the DOM, you listen on a stable parent like document so handlers still work after navigation.
How does the lesson capture a route parameter like '/user/:id'?
- With a regex on the whole URL
- With localStorage
- By reloading the page
- By splitting route and path into segments and capturing parts starting with ':'
Answer: By splitting route and path into segments and capturing parts starting with ':'. matchRoute splits both into segments; any segment starting with ':' is captured as a named parameter.
What is the benefit of lazy loading routes with dynamic imports?
- It removes the need for routing
- Users only download the JavaScript they need, speeding up the first load
- It disables caching
- It prevents XSS
Answer: Users only download the JavaScript they need, speeding up the first load. Dynamic imports load a view's module only when needed, so the initial bundle is much smaller.
How does the lesson recommend preventing XSS when inserting user data?
- Always use innerHTML with the raw input
- Disable JavaScript
- Use textContent or escape/sanitize user input before inserting it
- Store input in cookies
Answer: Use textContent or escape/sanitize user input before inserting it. Never insert raw user input as HTML; use textContent or escape/sanitize (e.g., DOMPurify) to prevent XSS.
Continue this course
- Previous: Module Bundlers: Webpack, Parcel & Vite
- Next: Back to Course