Reactivity Without a Framework: What Native JS Can Do Today
You want reactive UI behavior—state changes that automatically update the DOM—but you don’t want to ship 40KB of framework code for a simple widget. Good news: vanilla JavaScript reactivity is entirely achievable with APIs that have been stable in browsers for years.
This article covers the native tools available in late 2025 for building reactive UIs: Proxy-based reactive state, EventTarget and CustomEvent for pub/sub, and browser observers for DOM-aware reactions. You’ll learn what works today, what’s coming, and where these patterns map to framework internals.
Key Takeaways
- Proxy objects intercept property changes and enable automatic DOM updates without framework dependencies
- EventTarget and CustomEvent provide a native pub/sub layer for decoupled component communication
- Browser observers (MutationObserver, IntersectionObserver, ResizeObserver) handle DOM and layout reactivity
- The TC39 Signals proposal may standardize reactivity primitives, but current Proxy + EventTarget patterns achieve similar results today
What Reactivity Actually Means
Reactivity is a simple loop: state changes trigger UI updates. Frameworks automate this with virtual DOMs, compilers, or fine-grained dependency tracking. But the underlying mechanics rely on JavaScript features you can use directly.
The core pattern:
- Store state in a trackable structure
- Notify subscribers when state changes
- Update only the relevant DOM
Native browser APIs handle each step without external dependencies.
Proxy-Based Reactive State
The Proxy object intercepts property access and assignment. Combined with Reflect, it forms the foundation of proxy-based reactive state.
function createReactiveStore(initial, onChange) {
return new Proxy(initial, {
set(target, prop, value) {
const result = Reflect.set(target, prop, value)
onChange(prop, value)
return result
}
})
}
const state = createReactiveStore({ count: 0 }, (prop, value) => {
document.getElementById('count').textContent = value
})
state.count = 5 // DOM updates automatically
This pattern feels “signal-like”—you write to state, and effects run. Vue 3’s reactivity system uses Proxy internally for exactly this reason.
Limitation: Proxy traps fire only on mutations applied to the proxied object itself. If nested objects or arrays are not also wrapped in their own proxies, changes inside them (like array.push()) won’t be tracked. Many developers use immutable updates (e.g., state.items = [...state.items, newItem]) to guarantee updates trigger.
EventTarget and CustomEvent as a Pub/Sub Layer
For decoupled component communication, EventTarget provides a native pub/sub mechanism. Any object can become an event emitter.
const bus = new EventTarget()
// Subscribe
bus.addEventListener('state-change', (e) => {
console.log('New value:', e.detail)
})
// Publish
bus.dispatchEvent(new CustomEvent('state-change', {
detail: { count: 10 }
}))
This pattern powers reactive UI with native browser APIs. Components subscribe to events, react to changes, and stay decoupled. Unlike custom pub/sub implementations, EventTarget integrates with browser DevTools and follows standard event semantics.
Discover how at OpenReplay.com.
Browser Observers for DOM Reactivity
When you need to react to DOM or layout changes—not just state—browser observers fill the gap.
MutationObserver watches DOM modifications:
const observer = new MutationObserver((mutations) => {
mutations.forEach(m => console.log('DOM changed:', m))
})
observer.observe(document.body, { childList: true, subtree: true })
IntersectionObserver tracks element visibility—useful for lazy loading or analytics.
ResizeObserver responds to element size changes without polling.
These APIs have long been stable and are safe for production. They complement state-driven reactivity by handling cases where external factors modify the DOM.
The TC39 Signals Proposal: What’s Coming
There’s growing interest in standardizing reactivity primitives. The TC39 Signals proposal aims to define a common model that frameworks could share.
Important: As of 2025, this is still a proposal—not a shipped JavaScript feature. Frameworks like Solid, Angular, and Preact have adopted signal-like patterns, influencing the proposal’s design. But you cannot use “native signals” in browsers today.
The Proxy + EventTarget patterns above achieve similar goals. If signals standardize, migration should be straightforward since the mental model aligns.
Choosing the Right Pattern
| Pattern | Best For | Trade-off |
|---|---|---|
| Proxy | Local component state | Only tracks changes on the proxied object unless nested values are also proxied |
| EventTarget | Cross-component messaging | Manual wiring |
| MutationObserver | Reacting to external DOM changes | Performance overhead |
For small apps and widgets, combining Proxy-based state with EventTarget covers most reactive UI needs without framework overhead.
Conclusion
Reactivity without a framework is practical today. Proxy handles state tracking, EventTarget provides pub/sub, and browser observers react to DOM changes. These APIs are stable, well-documented, and compose into a lightweight reactive core.
You don’t need a framework to get fine-grained reactivity. You need to understand the primitives frameworks are built on—and now you do.
FAQs
Proxy traps only fire on direct property assignment to the proxied object. For nested objects, you need to either recursively wrap each nested object in its own Proxy, or replace the entire nested structure when making changes. Most developers opt for immutable update patterns like spreading to create new references.
EventTarget is a native browser API that integrates with DevTools and follows standard dispatch semantics. Full bubbling and capturing apply only when the event target is part of the DOM tree. Custom libraries may offer additional features like wildcard listeners or once-only subscriptions, but EventTarget requires no dependencies and works consistently across all modern browsers.
Use MutationObserver when you need to react to DOM changes made by external code, third-party scripts, or browser extensions. Proxy tracks JavaScript state changes you control. MutationObserver watches the actual DOM tree regardless of what caused the change. They serve different purposes and often work together.
The Signals proposal aims to standardize reactivity primitives that frameworks can share, not replace existing APIs. Proxy and EventTarget will remain valid approaches. If Signals ship, they will likely complement these patterns by providing a standard interface for fine-grained dependency tracking across different libraries.
Complete picture for complete understanding
Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.
Check our GitHub repo and join the thousands of developers in our community.