javascript-frameworks
December 21, 2025

State Management Without Framework Magic

Learn state management without frameworks. Build your own reactive system with vanilla JavaScript and understand when you need tools

7 min readReal-time tracking enabled
State Management Without Framework Magic
Share:

You know that feeling when you're using React or Vue, and state just... works? You update something, the UI updates, and it feels like magic. But here's the thing: it's not magic. It's just patterns. And you can build those patterns yourself with plain JavaScript.

Today, let's break down what state really is, how to manage it without any framework, and when you actually need those fancy tools.

What Even Is State?

State is just data that changes over time. That's it. Really.

When you're building a web app, state is everything that can change: the value in an input field, whether a modal is open, items in a shopping cart, or which tab is currently active. If it changes, it's state.

// This is state
let count = 0;
let isModalOpen = false;
let user = { name: "Alex", loggedIn: true };

The tricky part isn't storing this data (that's easy). The tricky part is making sure your UI updates when the data changes, and doing it in a way that doesn't turn into spaghetti code.

The Event-Driven Way of Thinking

Here's where things get interesting. JavaScript in the browser is built around events. Click a button? Event. Type in a field? Event. Load a page? You guessed it, event.

This event-driven approach is actually perfect for state management. Instead of manually updating the DOM everywhere, you can set up a system where state changes trigger events, and your UI listens for those events.

// Simple event-driven state
const state = {
  count: 0
};

function updateState(newCount) {
  state.count = newCount;
  // Trigger a custom event
  document.dispatchEvent(new CustomEvent('stateChange', { 
    detail: { count: state.count } 
  }));
}

// Listen for state changes
document.addEventListener('stateChange', (e) => {
  document.getElementById('counter').textContent = e.detail.count;
});

The beauty here is separation of concerns. Your state logic doesn't know anything about the DOM, and your UI code just reacts to changes.

Building Your Own State Manager

Let's build something more robust. A tiny state manager that you can actually use in real projects.

function createStore(initialState) {
  let state = initialState;
  const listeners = [];

  return {
    getState() {
      return state;
    },

    setState(updater) {
      // Support both direct values and updater functions
      state = typeof updater === 'function' 
        ? updater(state) 
        : { ...state, ...updater };
      
      // Notify all listeners
      listeners.forEach(listener => listener(state));
    },

    subscribe(listener) {
      listeners.push(listener);
      // Return unsubscribe function
      return () => {
        const index = listeners.indexOf(listener);
        listeners.splice(index, 1);
      };
    }
  };
}

// Using it
const store = createStore({ count: 0, user: null });

// Subscribe to changes
const unsubscribe = store.subscribe(state => {
  console.log('State changed:', state);
  document.getElementById('count').textContent = state.count;
});

// Update state
store.setState(state => ({ ...state, count: state.count + 1 }));

This pattern is basically what Redux does under the hood, just without the action types and reducers. Sometimes you need that structure, but often you don't.

Using the DOM as State

Here's a controversial take: sometimes the DOM itself is perfectly fine as state storage.

If you have a checkbox that controls something, the checked state of that checkbox IS the state. You don't always need to duplicate it in a JavaScript object.

// DOM as state
function getFormState() {
  return {
    email: document.getElementById('email').value,
    newsletter: document.getElementById('newsletter').checked,
    theme: document.querySelector('input[name="theme"]:checked').value
  };
}

// When you need it, just read from the DOM
document.getElementById('submit').addEventListener('click', () => {
  const formData = getFormState();
  console.log(formData);
});

This is actually how the web worked for years, and it still works fine for simpler interactions. The problem starts when you need that state in multiple places, or when the same state needs to drive multiple UI elements.

Using Proxies for Reactive State

Modern JavaScript has this cool feature called Proxies that lets you intercept operations on objects. You can use this to make state automatically reactive.

function reactive(target, callback) {
  return new Proxy(target, {
    set(obj, prop, value) {
      obj[prop] = value;
      callback(obj);
      return true;
    }
  });
}

// Create reactive state
const state = reactive({ count: 0 }, (newState) => {
  document.getElementById('count').textContent = newState.count;
});

// Now this automatically updates the UI
state.count = 5; // DOM updates automatically
state.count++; // DOM updates again

This is basically what Vue does with its reactivity system. It's elegant, but it has limitations (nested objects need special handling, arrays are tricky, etc.).

When Frameworks Actually Help

Okay, so if you can build all this yourself, why do frameworks exist?

They help when:

  1. Your UI is complex and deeply nested - Manually updating a deeply nested component tree is painful. Frameworks handle this with virtual DOM diffing.

  2. You need time-travel debugging - Tools like Redux DevTools let you replay state changes. Super useful for debugging complex apps.

  3. State changes are frequent and granular - If you're updating state 60 times per second (like in a game), a framework can batch updates efficiently.

  4. Team collaboration - Frameworks enforce patterns. When you hire a React dev, they know where to put things.

  5. You're building a long-lived application - The structure and tooling pay off over time.

When They Don't

Vanilla JS is better when:

  1. The app is simple - A todo list doesn't need Redux. Seriously.

  2. Performance is critical - Netflix switched parts of their UI to vanilla JS because framework overhead was slowing things down.

  3. Bundle size matters - Shipping 50KB of framework code to show a dropdown menu is overkill.

  4. You're learning - Understanding vanilla state management makes you a better framework developer.

  5. The project is unusual - Sometimes your use case doesn't fit the framework's assumptions.

A Practical Middle Ground

You don't have to choose between "no framework" and "full framework". There's a middle ground:

// Lightweight state + DOM updates
class Component {
  constructor(element, initialState) {
    this.element = element;
    this.state = initialState;
    this.render();
  }

  setState(updates) {
    this.state = { ...this.state, ...updates };
    this.render();
  }

  render() {
    // Override this in subclasses
    throw new Error('render() must be implemented');
  }
}

// Use it
class Counter extends Component {
  render() {
    this.element.innerHTML = `
      <div>
        <p>Count: ${this.state.count}</p>
        <button onclick="counter.setState({ count: ${this.state.count + 1} })">
          Increment
        </button>
      </div>
    `;
  }
}

const counter = new Counter(
  document.getElementById('app'), 
  { count: 0 }
);

This gives you component-like structure without a framework. For many projects, this is enough.

The Real Secret

The real secret of state management isn't React, Vue, or any framework. It's understanding these core concepts:

  • State is just data that changes
  • Changes should be predictable (one way to update, clear flow)
  • UI should react to state, not the other way around
  • Separation of concerns makes everything easier

Once you get these concepts, you can pick the right tool for the job. Sometimes that tool is React. Sometimes it's 20 lines of vanilla JavaScript. The important thing is that you're making that choice consciously, not just following the hype.

Frameworks are great. They solve real problems. But they're not magic, and you don't always need them. Understanding what's under the hood makes you better at using them when you do.

Resources

Enjoyed this article?

Vote or share it with someone who might love it.

George Ongoro
George Ongoro

Blog Author & Software Engineer

I'm George Ongoro, a passionate software engineer focusing on full-stack development. This blog is where I share insights, engineering deep dives, and personal growth stories. Let's build something great!

View Full Bio

Related Posts

Comments (0)

Join the Discussion

Please login to join the discussion