Back

Reactivity Without a Framework: What Native JS Can Do Today

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:

  1. Store state in a trackable structure
  2. Notify subscribers when state changes
  3. 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.

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

PatternBest ForTrade-off
ProxyLocal component stateOnly tracks changes on the proxied object unless nested values are also proxied
EventTargetCross-component messagingManual wiring
MutationObserverReacting to external DOM changesPerformance 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.

OpenReplay