@expressive/mvc
Class-based reactive state management - framework-agnostic core.
The core of Expressive MVC: reactive primitives built around plain classes, with no framework dependency. Provides the State model, instructions, context, and the renderer-agnostic Component that adapters like @expressive/react build on.
npm install @expressive/mvc
State
A State is a class whose fields are reactive - read to subscribe, assign to update. Getters are cached computed values.
import { State } from '@expressive/mvc';
class Counter extends State {
count = 0;
increment = () => this.count++;
get doubled() {
return this.count * 2; // recomputes only when count changes
}
}
const counter = Counter.new();
// effect runs now, then again whenever read values change
counter.get(({ count }) => {
console.log(`count is ${count}`);
});
counter.increment(); // -> "count is 1"
Models run and test anywhere - no renderer required.
Instructions
Field initializers that change how a property behaves:
ref() |
a mutable reference (e.g. a DOM node) that's still reactive |
set() |
computed values, smart setters, side-effects on assignment |
get() |
dependency injection - pull another State from context |
hot() |
a shallow-reactive array or object |
import { State, set, hot } from '@expressive/mvc';
class Cart extends State {
items = hot<Item[]>([]); // reactive collection
coupon = set('', code => apply(code)); // side-effect on assignment
}
Lifecycle
class Timer extends State {
seconds = 0;
new() { // runs once on activation
const id = setInterval(() => this.seconds++, 1000);
return () => clearInterval(id); // returned cleanup runs on destroy
}
}
const timer = Timer.new(); // construct + activate
timer.set(null); // destroy - runs cleanup, freezes state
Context & composition
States find each other through context, and compose by holding one another:
import { State, get } from '@expressive/mvc';
class Session extends State {
user = 'guest';
}
class Profile extends State {
session = get(Session); // injected from the nearest provider
address = new Address(); // nested state - its changes bubble up
}
An adapter (e.g. @expressive/react) supplies the provider that places a State in a tree; get() pulls it back out.
Events
Any property or arbitrary key can be dispatched and listened to:
const off = counter.set('refresh', () => reload()); // listen
counter.set('refresh'); // dispatch
off(); // stop listening
Framework-agnostic components
@expressive/mvc ships its own JSX runtime, so you can author components - including reusable libraries - against the core and let any host render them:
// tsconfig.json
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "@expressive/mvc"
}
}
Component - a State that renders - lives here as the agnostic base. For using it in an app (rendering, props, subcomponents, suspense), see @expressive/react.
To render state in a UI framework, add an adapter: @expressive/react or @expressive/preact.
Full guide and API reference → github.com/gabeklein/expressive-mvc
License
MIT