CodeCraft Chronicles

MEAT: Reactive State Without the Framework

MEAT (Mitt Enhanced Application Toolkit): A reactive developer toolkit for clean state control and plugin-powered architecture — Vue composables, DOM binding, a local storage adapter, and an event bus, all in a framework-agnostic package.

The Framework Tax

Modern front-end development often involves choosing a framework before choosing anything else. React, Vue, Svelte, Angular — each brings a reactivity model, a component system, a build pipeline, and opinions about everything from file naming to state management.

Sometimes the application is small enough that the framework tax — the bundle size, the learning curve, the breaking changes between versions — isn't worth paying. You need reactivity. You need state. You don't need a full framework.

MEAT is for those cases.

Core: Vue Composables

The reactive core is built around Vue 3's Composition API — specifically the parts that work without Vue's component system:

import { ref, computed, watch } from '@vue/reactivity'

// State that triggers updates when it changes
const count = ref(0)
const doubled = computed(() => count.value * 2)

watch(count, (newVal, oldVal) => {
  console.log(`Count changed from ${oldVal} to ${newVal}`)
})

count.value++  // triggers watch, recalculates doubled

@vue/reactivity is publishable as a standalone package — it doesn't require the Vue runtime. MEAT bundles it as the reactive foundation.

DOM Binding

The DOM binding layer connects reactive state to the DOM without a virtual DOM or template compilation:

import { bind, bindList, bindAttr } from 'meat'

const username = ref('')
const items    = ref([])

// Two-way binding for input
bind('#username-input', username)

// One-way binding for display
bind('#username-display', computed(() => `Hello, ${username.value}`))

// List rendering
bindList('#product-list', items, (item) => `
  <li data-id="${item.id}">
    <span class="name">${item.name}</span>
    <span class="price">$${item.price}</span>
  </li>
`)

// Attribute binding
bindAttr('#submit-btn', 'disabled', computed(() => username.value.length === 0))

Changes to the reactive state automatically update the DOM. DOM input events automatically update the reactive state. No templating language, no JSX, no compilation step.

Event Bus

The event bus is Mitt — a 200-byte event emitter — extended with:

import { createBus } from 'meat'

const bus = createBus()

// Define events with types
bus.on('user:login', (user) => {
  session.set('user', user)
  analytics.track('login', { userId: user.id })
})

bus.emit('user:login', { id: 1, email: 'test@example.com' })

// Middleware
bus.use((event, payload, next) => {
  console.log(`[${new Date().toISOString()}] ${event}`)
  next()
})

LocalStorage Adapter

The persistence layer syncs reactive state to localStorage with debouncing:

import { persist } from 'meat'

const cart = persist('cart', ref([]))
// cart is a reactive ref that reads from localStorage on init
// and writes to localStorage on change (debounced 300ms)

// Changes persist across page reloads
cart.value.push({ id: 'SKU-001', qty: 1 })

The debouncing prevents write storms when state changes rapidly — form typing, drag operations, slider values.

Plugin System

MEAT's plugin system lets you extend or replace any part of the toolkit:

import { createApp } from 'meat'

const app = createApp({
  plugins: [
    devTools(),              // Vue DevTools integration
    router(routes),          // Hash-based routing
    i18n({ locale: 'es' }), // Internationalization
    analytics(trackingId),   // Event tracking
  ],
})

Each plugin receives the reactive state, event bus, and DOM binding utilities. Plugins compose rather than conflict.

When MEAT Fits

MEAT is right when:

It's the wrong tool when you're building a large SPA where Vue, React, or Svelte's ecosystem — DevTools, routing, SSR, component libraries — becomes an asset rather than overhead.

License

MIT

Comments