npm.io
0.80.0 • Published 3d ago

@expressive/mvc

Licence
MIT
Version
0.80.0
Deps
0
Size
191 kB
Vulns
0
Weekly
0
Stars
101

@expressive/mvc

Class-based reactive state management - framework-agnostic core.

NPM


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