Rethinking Reactivity in JavaScript: No Frameworks, No Libraries

12 min read
javascript reactivity proxy

As frontend developers, we’re constantly surrounded by new tools, frameworks, and state management libraries that promise to make our lives easier. And while many of them are great, I believe it’s essential to pause and ask ourselves: Do I really need this tool? Or can I solve this with native JavaScript?

In this post, I want to share a perspective that’s become increasingly important in my journey as a developer: learning to understand the language at its core — and using what it already gives us before reaching for the latest third-party solution.

Let’s talk about reactivity, and how we can build it from scratch using only the native JavaScript Proxy, Reflect & CustomEvent APIs.


Why We Should Start with the Language Itself

As developers, we often turn to libraries like Redux, Zustand, or React Context to manage the state of our applications. These tools make our work much easier, but it’s essential to understand what happens under the hood.

All these state management libraries implement some form of reactivity, and many use the native JavaScript APIs we’ll see in this article. Understanding these foundations provides us with several advantages:

Moreover, the source code of these libraries is built on the same foundations that we’ll explore here. That’s why diving into JavaScript’s native APIs will make us better developers.


What is Reactivity?

Reactivity is a programming pattern that allows us to automatically respond to changes in data. It’s the core idea behind frameworks like Vue, React (with its reconciliation), or Svelte.

The concept is simple: when a value changes, we want something else to happen as a result — like updating the UI or triggering a calculation.

With modern JavaScript, we can implement this ourselves using native tools like Proxy and Reflect.


Creating a Simple Reactive Store with Proxy

Let me walk you through a minimal example of a reactive store — something like the core of Zustand or Vuex, but written from scratch.

Here’s a version where we pass a callback that reacts whenever the state changes:

function createReactiveStore(initialState, callback) {
    return new Proxy(initialState, {
        set(target, prop, value) {
            const success = Reflect.set(target, prop, value);
            if (success) {
                callback(prop, value);
            }
            return success;
        },
    });
}

const store = createReactiveStore(
    {
        selectedSneaker: null,
        cart: [],
        stock: {
            "nike-air-max": 5,
            "adidas-ultraboost": 3,
        },
    },
    (key, value) => {
        console.log(`"${key}" was updated to:`, value);
    },
);

This works, but let’s take it one step further and make it more reactive in the browser.


Emitting Custom Events on State Changes

Instead of using a single callback, we can emit a CustomEvent every time a specific property is updated. This way, any part of our app can listen for these events and update the UI accordingly — no tight coupling, no global state libraries.

function createReactiveStore(initialState) {
    const eventTarget = new EventTarget();

    const proxy = new Proxy(initialState, {
        set(target, prop, value) {
            const success = Reflect.set(target, prop, value);
            if (success) {
                const event = new CustomEvent(`store:${prop.toString()}`, {
                    detail: value,
                });
                eventTarget.dispatchEvent(event);
            }
            return success;
        },
    });

    return {
        state: proxy,
        onChange: (prop, handler) => {
            eventTarget.addEventListener(`store:${prop}`, handler);
        },
    };
}

Example: Sneakers Store App

Let’s say we’re building a sneaker store app. Here’s how we could use this store to track and react to UI changes:

const { state, onChange } = createReactiveStore({
    selectedSneaker: null,
    cart: [],
    stock: {
        "nike-air-max": 5,
        "adidas-ultraboost": 3,
    },
});

// Reacting to state changes
onChange("selectedSneaker", (event) => {
    document.querySelector("#selected-sneaker").textContent = `Selected model: ${event.detail}`;
});

onChange("cart", (event) => {
    document.querySelector("#cart-count").textContent = `Cart: ${event.detail.length} items`;
});

// Updating the state
state.selectedSneaker = "nike-air-max";
state.cart = [...state.cart, "nike-air-max"];

It’s minimal, fast, and works entirely with browser-native features.


Why I Love This Approach

This kind of DIY approach has helped me understand why frameworks exist — and when I truly need them.


Final Thoughts

The Proxy and Reflect objects combined with a specific events system provide us with a powerful and elegant way to implement reactivity in pure JavaScript. The use of semantic events allows us to create clearer, more maintainable, and efficient systems.

Using this type of reactive system has allowed me to create more maintainable applications with better performance, especially in medium-sized projects where a complete framework might be excessive.

This isn’t about reinventing the wheel or rejecting modern tools. It’s about building intuition, understanding how the language works, and realizing that you can often go surprisingly far without reaching for a library.

Learning these native patterns has made me a better developer, and I encourage anyone working in frontend to take the time to explore them. You’ll not only become more self-sufficient — you’ll also appreciate the design of libraries and frameworks much more.


References & Further Reading