Expressive MVC
A class-based state backbone for modern UI applications
Expressive MVC is a reactive state library built around plain classes. It's designed to be the backbone of your application - the place where data, behavior, and lifecycle live - so components can go back to doing what they're best at: describing UI.
import State from '@expressive/react';
class Counter extends State {
count = 0;
increment() {
this.count++;
}
decrement() {
this.count--;
}
}
function CounterWidget() {
const { count, increment, decrement } = Counter.use();
return (
<div>
<button onClick={decrement}>-</button>
<span>{count}</span>
<button onClick={increment}>+</button>
</div>
);
}No reducers. No selectors. No dependency arrays. No extra libraries for async, context, or computed values. Just a class and a hook.
Every concept in these docs has a live, editable counterpart in the Playground - when a page links one, try breaking it.
Why it exists
Most React apps outgrow their state. What starts as useState sprawls into useEffect chains, useCallback memoization, custom hooks that return objects of hooks, then a store library, then middleware for async, then a query client. By the time a feature is "done", its logic is scattered across five different primitives - none of which are easy to test, reuse, or understand in isolation.
Expressive takes a different approach: put the logic back in one place. Display-agnostic models live in State classes. Behavior that only exists for one rendered unit can live directly in a Component class. Either way - values, methods, lifecycle, and rendering boundary are explicit, instead of scattered across hooks.
What you get from a single import
- Reactive properties - plain class fields that components subscribe to automatically.
- Computed values - getters that update when the values they read change.
- Async + Suspense - declarative data loading, integrated with Suspense out of the box.
- Context - the class itself is the context key. Fully typed, no wiring.
- Lifecycle - one
new()hook for setup and teardown. No dependency arrays. - Components - the
Componentclass lets state render itself, with built-in error boundaries and subcomponents. - Testability - state classes are plain objects. Test them without a renderer, without
act(), without a DOM.
Where to go next
Getting Started
Install, build your first class, learn the mental model.
Why Classes?
The organizational argument for moving state out of components.
Migrating from Hooks
A step-by-step playbook for adopting Expressive in an existing codebase.
API Reference
Every method, instruction, and hook.
Building with an agent?
npx skills add gabeklein/expressive-mvcgives it the full API reference as a skill.