SkillAgentSearch skills...

expressive-mvc

Class-based reactive state management for React (Expressive MVC)

Install / Use

npx skills add gabeklein/expressive-mvc --skill skills

Installs into whichever agent you are using.

About this skill
📄

SKILL.md

Installable skill definition

Quality Score

77/100

Supported Platforms

Universal

Our assessment of expressive-mvc

expressive-mvc scores 77/100 on our quality scale, 1071st of 1,189 Content & Media skills we index.

Its SKILL.md is 21 KB long, well organised into 24 sections with 13 code examples: a thorough specification that gives an agent plenty to work with.

It has no GitHub stars yet, so there is no community track record; judge it on its content.

Substance
30/30
Structure
20/20
Description
12/15
Adoption
0/20
Freshness
15/15

Maintenance, license and trust

  • The repository was last updated today, so expressive-mvc is actively maintained.
  • Our last check on 2026-09-15 found the source still online.
  • No license is declared. By default that means all rights are reserved: you can read it, but reusing or redistributing it is not clearly permitted. Ask the author before building on it commercially.
  • Its trust signals score 80/100, with 2 cautions from licensing, adoption, age or documentation. These come from repository metadata, not a code audit — read the skill file before letting an agent act on it.

expressive-mvc compared with similar skills

All 4 of these similar skills score higher than expressive-mvc; compare them before choosing.

SkillScoreStarsUpdatedFormat
expressive-mvc (this skill)by gabeklein770todaySKILL.md
siyuanby siyuan-note10047.0ktodayMCP Server
algorithmic-artby anthropics100177.9k34d agoSKILL.md
pptxby anthropics100177.9k34d agoSKILL.md
math-olympiadby anthropics10043.1k8d agoSKILL.md

Frequently asked questions

How do I install expressive-mvc?
Run npx skills add gabeklein/expressive-mvc --skill expressive-mvc. The install tabs above show the steps for each supported agent.
Which AI agents does expressive-mvc work with?
It is written for Universal, as a SKILL.md file. Other agents that read the same format can often use it too.
Is expressive-mvc safe to use?
It declares no license and scores 80/100 on trust signals. Skills are instructions an agent will follow, so read the file before installing it and do not approve commands you do not understand.
Is expressive-mvc still maintained?
The repository was last updated today, so expressive-mvc is actively maintained.

name: expressive-mvc description: Class-based reactive state management for React (Expressive MVC). Use when writing or refactoring React state - converting useState/useEffect/useMemo hooks, fixing prop drilling, choosing state ownership (State vs Component, has-pool domain rows, region controllers), dependency snapshots, presence boundaries with get(true), Provider/context, async suspense, router - and when auditing a codebase for fit.

Expressive MVC

Class-based reactive state for React. State classes define reactive properties, computed values, async data, and context - all as plain class fields using instruction helpers.

Packages

| Package | Status | Description | | -------------------- | --------- | ----------------------------------------------------------------- | | @expressive/react | Published | React adapter. Primary import for State, Component, instructions. | | @expressive/mvc | Published | Framework-agnostic core. Rarely imported directly. | | @expressive/preact | Private | Thin wrapper over React adapter via preact/hooks. Prerelease. | | @expressive/router | Published | Host-agnostic, class-based router built on MVC. |

Installing

For a React app, @expressive/react is the only install:

npm install @expressive/react

@expressive/mvc arrives as its dependency - do not add it to package.json as well. Import State, Component, and every instruction (set, get, ref, map, has, def) from @expressive/react; the adapter re-exports the core. Install @expressive/mvc directly only when writing host-agnostic code that must not depend on a UI adapter - a shared domain package, a Node service, or a new adapter. Add @expressive/router alongside the adapter when you want routing.

Start With Ownership, Not APIs

The most common failure when adopting this library is translating hooks one-for-one before deciding who owns each behavior. Resolve ownership first; APIs come second.

For every stateful concern, pick exactly one owner:

  • Component - state intrinsic to one display subtree: controls, shells, panels, editors, review/confirm surfaces. Usually defines render(). Fields, handlers, and rendering live on one class.
  • State - headless model or workflow: network operations, domain rules, cross-view coordination. Views subscribe via State.get() / State.use().
  • Plain function component - simple presentation, or trivial local UI state. Not everything needs a class.

Concerns include the rows: UI state describing an entry in a collection - selection, status, progress - is its own State or Component spawned in a has pool; actions about item belong on item. Id-keyed fields (Record<Id, T>, parallel Maps) and (id, value) methods are a missing class; subsets are a second pool admitting the same members.

Counter-rules:

  • Do not create FooState plus FooView just because hooks were present. If the behavior and rendering are one unit, class Foo extends Component is the refactor.
  • Avoid Component where a provided State suffices - Components carry React instance surface (props, state, setState, forceUpdate) that makes .get() IntelliSense noisier.
  • A render-less Component (children pass through while providing context and boundary placement) is only for cases where React tree placement is the feature: route controllers, progressive boundaries.
  • A provided State implicitly provides its child States - prefer theme = new Theme() on an existing owner over stacking Providers for every small controller.

Golden-Path Refactor Algorithm

Before refactoring existing React code, read react/refactor.md - it expands every step with before/after examples and ends with the review checklist. The short form:

  1. Identify lifecycle and ownership boundaries before translating any hooks.
  2. Separate headless workflow state from display-intrinsic state.
  3. Choose State, Component, or a plain function component for each owner.
  4. Give every repeated UI entry own class in a has pool; actions about item belong on item.
  5. Split unrelated clusters remaining on page State to owned region States composer = new Composer().
  6. Provide classes directly <Provider for={AppState}> never an instance if only to provide it.
  7. Move source fields and behavioral methods first; do not mechanically translate setters.
  8. Keep shared, semantic derivations as getters; leave single-consumer display derivations in their consuming component.
  9. Let contextual children call .get() instead of receiving drilled props.
  10. At every .get() / .use(), destructure for exact nested dependency snapshot.
  11. May assign thru subscribed proxies; is only to retain the root object alongside sibling destructuring.
  12. Gate optional children at call site; inside, assert requirements with .get(true).
  13. Extract long conditional JSX to named scopes, then consolidate scopes that share dependencies and hold no nested logic.
  14. Audit the result against the checklist in react/refactor.md.

For large apps, scope a one-shot conversion to route/page controllers and their domain pools; leave mature leaf widgets on hooks until parent domains stabilize.

Write output in the conventions of react/style.md. They are opinion, not semantics - but they exist to keep reactive dependencies auditable, and the golden path applies them by default.

Core API

import State, {
  Component,
  ref,
  def,
  get,
  has,
  map,
  set,
  transition,
  Consumer,
  Provider
} from '@expressive/react';

State Class

Extend State to define reactive models. Use State.new() to construct a root instance - it constructs and activates, which plain new does not. Bare new is correct in one place: a class field on another State (theme = new Theme()), where the owner adopts and activates the child, and destroys it in turn.

class Counter extends State {
  count = 0;

  increment() {
    this.count++;
  }
}

const counter = Counter.new();
counter.count; // 0
counter.increment();
counter.count; // 1

Properties assigned in the class body are reactive - updates notify subscribers. Methods are auto-bound.

Instructions & Reactive Helpers

Field initializers that configure reactive behavior. Each has multiple overloads - fetch the reference when a task needs them.

| Helper | Use for | Reference | | ------- | ------------------------------------------------------------------------------------------------------ | -------------------------- | | set() | Defaults, placeholders (suspend until assigned), lazy/async factories (suspense), setter callbacks and validation | field/set.md | | get() | Context lookup between States - required or optional upstream, downstream collection | field/get.md | | ref() | Mutable refs (.current), ref callbacks with cleanup, ref proxies | field/ref.md | | map() | Reactive Map field - keyed entries or a keyed spawner, with owned State members and direct render | field/map.md | | has() | Owned collections - an ordered list of values, or a pool of spawned members. Pools are for per-item UI state (selection, progress, row actions) | field/has.md | | def() | Low-level custom property behavior | field/def.md |

For computed values, declare a normal class getter - getters on a State subclass are auto-promoted to memoized, dependency-tracked properties. See state/computed.md for tracking rules and when a derivation should not be a getter.

Do not pass a bare promise to set(). Use set(() => promise) or set(async () => value) so work starts during activation/access instead of construction.

class UserProfile extends State {
  userId = set<string>();

  user = set(async () => {
    const res = await fetch(`/api/users/${this.userId}`);
    return res.json();
  });

  email = set('', (value) => {
    if (!value.includes('@')) throw false;
  });

  get displayName() {
    return `${this.user.firstName} ${this.user.lastName}`;
  }
}

React Hooks

// Local state - creates instance, owns lifecycle, subscribes to accessed fields
function MyComponent() {
  const { count, increment } = Counter.use();
  return <button onClick={increment}>{count}</button>;
}

// Context state - reads nearest Provider, subscribes reactively
function Child() {
  const { count } = Counter.get();
  return <span>{count}</span>;
}

// An instance you already hold - place it; it renders and subscribes itself
function Parent({ counter }: { counter: Counter }) {
  return <section>{counter}</section>;
}

Static .use() and .get() are React hooks - call unconditionally at the top of a component or render(), never inside branch, event handlers, or loops. They build green when misused and crash at runtime. (Instance method get is not a hook.)

Use State.use() when the component should create and own the instance. Use State.get() when the instance comes from context. To render an instance you already hold, make it a Component and place as {instance} - subscription belongs to the placed instance, not the surrounding function. See react/react.md for overloads (optional lookup, required values, computed selector).

The Dependency Snapshot

Open every subscribing component by destructuring the exact reactive values it renders - nested ones included. Nested observable reads are proxied and tracked automatically, so nested destructuring subscribes to child fields; a child reached through a parent proxy needs no separate subscription.

function OrderSummary() {
  const {
    status,
    customer: {
      name,
      address: {
        city,
      } = {},
    },
  } = Order.get();

  return <p>{name} ({city ?? 'no address'}) - {status}</p>;
}

This is the norm, not a preference:

  1. The component's complete dependency surface is visible at the top - reviewable at a glance.
  2. Each trapped getter is traversed once, instead of re-walking order.customer.address.city in every expression.
  3. Reads create subscriptions. A deep read buried in a conditional branch subscribes only on renders where that branch runs - a conditional subscription. Hoisting reads into the snapshot makes the dependency surface deterministic.

Optional nested objects take in-place defaults (= {}) rather than a separate unwrap step. The same rule applies to this inside Component.render() and subcomponents.

Transparent Writes

Subscription proxies pass assignments through to the real instance - no unwrapping needed:

const form = LoginForm.get();          // whole object is the only need - take it directly

<input
  value={form.username}
  onChange={(e) => (form.username = e.target.value)}
/>

Nested objects reached through a snapshot are equally writable:

const { transfer, confirmed } = ReviewStep.get();

<button onClick={() => (transfer.step = 'generate')} disabled={!confirmed} />

Use is only when retaining the root object alongside sibling values from the same snapshot:

const { is: review, confirmed, hasBlocking } = ReviewStep.get();

Do not unwrap every writable object through is - that is the most common misuse.

Presence Boundaries & get(true)

When a child's content requires values that may not exist yet, the parent owns the gate and the child asserts the invariant with get(true):

function SettingsContent() {
  const { draft } = SettingsState.get();

  return

Truncated for display — read the full file on GitHub.

Related Skills

View on GitHub
GitHub Stars0
CategoryContent
Updated9h ago
Forks0

Trust signals

80/100

From repository metadata: license, adoption, age and documentation. Not a code audit — see the Safety scan above for what the skill file itself contains.

1 medium1 low