expressive-mvc
Class-based reactive state management for React (Expressive MVC)
Install / Use
npx skills add gabeklein/expressive-mvc --skill skillsInstalls into whichever agent you are using.
SKILL.md
Installable skill definition
Quality Score
Category
Content & MediaSupported Platforms
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.
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.
| Skill | Score | Stars | Updated | Format |
|---|---|---|---|---|
| expressive-mvc (this skill)by gabeklein | 77 | 0 | today | SKILL.md |
| siyuanby siyuan-note | 100 | 47.0k | today | MCP Server |
| algorithmic-artby anthropics | 100 | 177.9k | 34d ago | SKILL.md |
| pptxby anthropics | 100 | 177.9k | 34d ago | SKILL.md |
| math-olympiadby anthropics | 100 | 43.1k | 8d ago | SKILL.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.
Skill content
View source on GitHubname: 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 definesrender(). Fields, handlers, and rendering live on one class.State- headless model or workflow: network operations, domain rules, cross-view coordination. Views subscribe viaState.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
FooStateplusFooViewjust because hooks were present. If the behavior and rendering are one unit,class Foo extends Componentis the refactor. - Avoid
Componentwhere a providedStatesuffices - 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:
- Identify lifecycle and ownership boundaries before translating any hooks.
- Separate headless workflow state from display-intrinsic state.
- Choose
State,Component, or a plain function component for each owner. - Give every repeated UI entry own class in a
haspool; actions about item belong on item. - Split unrelated clusters remaining on page State to owned region States
composer = new Composer(). - Provide classes directly
<Provider for={AppState}>never an instance if only to provide it. - Move source fields and behavioral methods first; do not mechanically translate setters.
- Keep shared, semantic derivations as getters; leave single-consumer display derivations in their consuming component.
- Let contextual children call
.get()instead of receiving drilled props. - At every
.get()/.use(), destructure for exact nested dependency snapshot. - May assign thru subscribed proxies;
isonly to retain the root object alongside sibling destructuring. - Gate optional children at call site; inside, assert requirements with
.get(true). - Extract long conditional JSX to named scopes, then consolidate scopes that share dependencies and hold no nested logic.
- 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:
- The component's complete dependency surface is visible at the top - reviewable at a glance.
- Each trapped getter is traversed once, instead of re-walking
order.customer.address.cityin every expression. - 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
siyuan
47.0kAn open-source, privacy-first, self-hosted knowledge workspace where humans and AI agents work together 开源、隐私优先、自托管的知识工作空间,让人与智能体在此协作
algorithmic-art
177.9kCreating algorithmic art using p5.js with seeded randomness and interactive parameter exploration. Use this when users request creating art using code, generative art, algorithmic art, flow fields, or particle systems.
pptx
177.9kUse this skill any time a .pptx or .potx file is involved in any way — as input, output, or both. This includes: creating slide decks, pitch decks, or presentations; reading, parsing, or extracting text from any .pptx or .potx file (even if the extracted content will be used elsewhere, like in an em…
math-olympiad
43.1k"Solve competition math problems (IMO, Putnam, USAMO, AIME) with adversarial verification that catches the errors self-verification misses.
Trust signals
From repository metadata: license, adoption, age and documentation. Not a code audit — see the Safety scan above for what the skill file itself contains.
