Quartz SPA Navigation Architecture
This document describes the Single Page Application (SPA) navigation system used in this Quartz-based knowledge vault and the patterns for building components that work reliably with it.
Overview
Quartz uses a custom SPA router built on micromorph for DOM morphing. When users click internal links, instead of full page reloads, the router:
- Fetches the new page content
- Fires a
prenavevent for cleanup - Runs registered cleanup functions
- Morphs the DOM to the new state
- Fires a
navevent for (re)initialization
Key Events
nav Event
Fired after navigation completes. Components should register listeners for this event to initialize or reinitialize themselves.
document.addEventListener('nav', async (e: CustomEventMap['nav']) => {
const currentSlug = e.detail.url;
await initializeComponent(currentSlug);
});prenav Event
Fired just before navigation occurs. Use this for saving state that should persist across navigations (e.g., scroll position).
document.addEventListener('prenav', () => {
saveScrollPosition();
});Cleanup System
Registering Cleanup Functions
Use window.addCleanup() to register functions that should run before the next navigation:
const handler = () => doSomething();
element.addEventListener('click', handler);
window.addCleanup(() => element.removeEventListener('click', handler));Cleanup Function Requirements
- Must not throw - Cleanup functions are isolated in try-catch, but throwing disrupts the expected cleanup flow
- Same function reference - When removing event listeners, you must use the same function reference that was added
// WRONG - creates new function, won't remove listener
icon.addEventListener('click', () => renderGraph());
window.addCleanup(() => icon.removeEventListener('click', () => renderGraph()));
// CORRECT - uses same function reference
const handleClick = () => renderGraph();
icon.addEventListener('click', handleClick);
window.addCleanup(() => icon.removeEventListener('click', handleClick));Component Initialization Pattern
Recommended Approach
document.addEventListener('nav', async (e: CustomEventMap['nav']) => {
try {
await initializeComponent(e.detail.url);
} catch (error) {
console.error('[ComponentName] Initialization failed:', error);
}
});Why This Works
The SPA router defers the initial nav event until either:
- The page
loadevent fires (if document isn’t ready) - A microtask completes (if document is already loaded)
This ensures component scripts have time to register their nav event listeners before the initial event fires.
Common Pitfalls
1. Timing-Based Initialization
Don’t do this:
// Anti-pattern: multiple fallback timers
requestAnimationFrame(() => init());
setTimeout(() => init(), 50);
setTimeout(() => init(), 200);
window.addEventListener('load', () => init());This pattern is unreliable and indicates a race condition problem. If you need this, the root cause should be fixed instead.
2. Self-Cleanup Arrays
Don’t do this:
// Anti-pattern: manual cleanup tracking
let cleanups: (() => void)[] = [];
cleanups.push(setupFeature());
// later...
cleanups.forEach((fn) => fn());
cleanups = [];Do this instead:
// Use the built-in cleanup system
const cleanup = setupFeature();
window.addCleanup(cleanup);3. Missing Error Handling
Always wrap async initialization in try-catch to prevent unhandled rejections:
document.addEventListener('nav', async (e) => {
try {
await heavyInitialization(e.detail.url);
} catch (error) {
console.error('[Component] Init failed:', error);
}
});Technical Details
Script Execution Order
spa.inline.ts- Creates router and defers initialnavevent- Component scripts - Register their
navlisteners - Initial
navevent fires - All components initialize
DOM Morphing
Micromorph intelligently patches the DOM rather than replacing it entirely. This means:
- Elements with
data-persistattribute survive navigation - Focus state may be preserved in some cases
- Existing event listeners on morphed elements are replaced
Navigation Function
Programmatic navigation is available via:
window.spaNavigate(new URL(targetPath, window.location.toString()));Files Reference
quartz/components/scripts/spa.inline.ts- SPA router and cleanup systemquartz/components/scripts/explorer.inline.ts- Explorer component (example)quartz/components/scripts/graph.inline.ts- Graph component (example)
Related
- quartz-type-safety - TypeScript type safety patterns for Quartz
- global-type-augmentation - Global type declaration pattern