npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

mvc-kit

v4.5.2

Published

MVVM application framework: a tiny zero-dependency reactive core with opt-in forms, UI helpers, storage adapters, and bindings for React, Solid, and web components

Readme

mvc-kit

mvc-kit

An MVVM application framework: a tiny zero-dependency reactive core plus opt-in app-layer entries — forms, UI helpers, storage adapters, headless components — and thin bindings for React, Solid, and web components over one framework-agnostic contract.

  • Tiny core — the mvc-kit entry is ~14 concepts of pure reactive state management; everything else is opt-in
  • Zero dependencies — React and Solid are optional peers; storage backends are injected by you
  • Framework-agnostic — the same ViewModel binds to React, Solid, and custom elements unchanged
  • TypeScript-first — typed state, typed events, typed async tracking
  • Pluggable — adapter-based storage, plugin-based observability, a documented binding contract for new frameworks

Installation

npm install mvc-kit

Quick Start (60 seconds)

Write a ViewModel once. State is the source of truth, derived values are getters, async tracking and method binding are automatic:

import { ViewModel } from 'mvc-kit';

interface CounterState {
  count: number;
}

class CounterViewModel extends ViewModel<CounterState> {
  get double(): number {
    return this.state.count * 2;
  }

  increment() {
    this.set({ count: this.state.count + 1 });
  }
}

Bind it in React:

import { useLocal } from 'mvc-kit/react';

function Counter() {
  const vm = useLocal(CounterViewModel, { count: 0 });
  return (
    <button onClick={vm.increment}>
      {vm.state.count} (doubled: {vm.double})
    </button>
  );
}

The same class in Solid — no changes to the ViewModel:

import { createLocal } from 'mvc-kit/solid';

function Counter() {
  const vm = createLocal(() => new CounterViewModel({ count: 0 }));
  return (
    <button onClick={vm.increment}>
      {vm.state.count} (doubled: {vm.double})
    </button>
  );
}

And the same class as a web component — no framework at all:

import { ViewModelElement } from 'mvc-kit/element';

class CounterElement extends ViewModelElement<CounterViewModel> {
  protected createInstance() {
    return new CounterViewModel({ count: 0 });
  }

  protected render(vm: CounterViewModel) {
    this.textContent = `${vm.state.count} (doubled: ${vm.double})`;
  }

  connectedCallback() {
    super.connectedCallback();
    this.onclick = () => this.vm.increment();
  }
}

customElements.define('x-counter', CounterElement);

This is the point of mvc-kit: your application logic — state, derivation, async orchestration, validation — lives in plain TypeScript classes that know nothing about the UI framework. The bindings are thin: construct → subscribe → init()dispose(). That contract is public and documented, so the community can add frameworks (docs/extending.md).

Entry-Point Map

The core entry is the actual "reactive state management" library. Everything else is an opt-in app-layer bundle — import it only when you need it; tree-shaking and preserveModules keep unused entries out of your bundle.

| Entry | What's in it | Concepts | When you need it | |-------|--------------|----------|------------------| | mvc-kit | ViewModel, Model, Collection, PersistentCollection (+ StorageAdapter contract), Resource, Service, EventBus, Channel, Controller, Trackable, singleton registry (+ SSR dehydrate/hydrate), installPlugin, error utilities | ~14 | Always — this is the framework-agnostic core | | mvc-kit/react | useLocal, useSingleton, useInstance, useModel, useFormModel, useField, useEvent, useTeardown, Provider/useResolve | 9 | React apps (hooks only — no components) | | mvc-kit/react/ui | DataTable, CardList, InfiniteScroll | 3 | Headless, unstyled table/list/scroll components | | mvc-kit/forms | FormModel, attemptSubmitAll | 2 | Interactive forms (touched gating, submit reveal, server errors) | | mvc-kit/helpers | Sorting, Pagination, Selection, Feed, Pending, ValueResource, PersistentValue | 7 | Composable UI-state helpers for tables, feeds, retry queues, single-value records, one persisted record | | mvc-kit/offline-kit | createOfflineKit, Outbox, SyncController, SyncStatusViewModel, Connectivity | 5 | Offline-first apps — a durable mutation queue (queue offline, sync on reconnect, retry/backoff); a write-side outbox, not a sync engine | | mvc-kit/storage | localStorageAdapter, sessionStorageAdapter, indexedDBAdapter, asyncStorageAdapter | 4 | Persisting a PersistentCollection to browser/device storage | | mvc-kit/solid | createLocal, createSingleton, reactive | 3 | Solid apps | | mvc-kit/element | ViewModelElement | 1 | Web components / no framework | | mvc-kit/testing | flushAsync, waitFor, waitForAsync, seedSingletons (+ re-exports of singleton, hasSingleton, teardownAll, provideSingleton, dehydrate, hydrate) | 4 | Test suites | | mvc-kit/devtools | loggerPlugin, inspectRegistry, attachDevtools | 3 | Debugging and observability |

Core Classes & Roles

| Class | Role | Entry | Scope | |-------|------|-------|-------| | ViewModel<S, E?> | Reactive state + computed getters + async tracking + typed events | mvc-kit | Component-scoped (useLocal / createLocal) | | Model<S> | Generic editable entity: validation + dirty tracking + commit/rollback | mvc-kit | Component-scoped (useModel) | | FormModel<S> | Model + form-presentation state (touched gating, submit reveal, server errors, per-field validating) | mvc-kit/forms | Component-scoped (useFormModel + useField) | | Collection<T> | Reactive typed array, shared data cache, optimistic updates, eviction & TTL | mvc-kit | Singleton | | PersistentCollection<T> | Collection + storage persistence via an injected StorageAdapter | mvc-kit (adapters in mvc-kit/storage) | Singleton | | Resource<T> | Collection + async tracking, transparent Collection injection | mvc-kit | Singleton | | Service | Stateless infrastructure adapter (HTTP, storage) | mvc-kit | Singleton | | EventBus<E> | Typed pub/sub for cross-cutting events | mvc-kit | Singleton | | Channel<M> | Persistent connection (WebSocket/SSE) with auto-reconnect | mvc-kit | Singleton | | Controller | Stateless multi-ViewModel orchestrator (rare) | mvc-kit | Component-scoped | | Trackable | Base class for custom reactive objects — subscribable + disposable + auto-bind | mvc-kit | ViewModel property / useLocal | | Sorting<T> | Multi-column sort state + apply pipeline | mvc-kit/helpers | ViewModel property | | Pagination | Page/pageSize state + array slicing | mvc-kit/helpers | ViewModel property | | Selection<K> | Key-based selection set with toggle/select-all | mvc-kit/helpers | ViewModel property | | Feed<T> | Cursor + hasMore + item accumulation for server-side pagination | mvc-kit/helpers | ViewModel property | | Pending<K, Meta?> | Per-item operation queue with retry + status tracking | mvc-kit/helpers | Resource property | | ValueResource<T> | Single-value async record — "Resource, but scalar" (one value + async tracking + lifecycle) | mvc-kit/helpers | Singleton / ViewModel property | | PersistentValue<T> | Single persisted record — "PersistentCollection, but scalar" (one value + storage, always defaulted) | mvc-kit/helpers | Singleton / ViewModel property |

Interfaces

| Interface | Description | |-----------|-------------| | Subscribable<S> | Has state, subscribe(), dispose(), disposeSignal | | Disposable | Has disposed, disposeSignal, dispose() | | Initializable | Has initialized, init() | | StorageAdapter<T> | load(key) / save(key, items) / remove(key) — sync or async | | MvcKitPlugin | Named bag of optional observability hooks (onCreate, onStateChange, onAsync, …) | | TaskState | { loading: boolean; error: string \| null; errorCode: AppError['code'] \| null; cause: unknown } | | Listener<S> | (state: S, prev: S) => void | | Updater<S> | (prev: S) => Partial<S> \| void | | ValidationErrors<S> | Partial<Record<keyof S, string>> | | AsyncMap<T> | Maps each async method key of T to its TaskState (the shape of vm.async) |

Singleton Registry & SSR

| Function | Description | |----------|-------------| | singleton(Class, ...args) | Get or create singleton (uses static DEFAULT_STATE when present and no args given) | | provideSingleton(Class, instance) | Seed the registry with a specific instance (test injection) | | hasSingleton(Class) | Check if a live singleton exists | | teardown(Class) / teardownAll() | Dispose and remove singleton(s) | | dehydrate() | Serialize singleton data on the server (instances opt in via toJSON(); payload keyed by static SSR_KEY, falling back to class name) | | hydrate(data) | Seed singleton state on the client from the server payload |

onInit() data loading runs on the client only — server data crosses the boundary via dehydrate()/hydrate(). See BEST_PRACTICES.md → SSR.

Error Utilities

| Export | Description | |--------|-------------| | AppError (type) | Canonical error shape; AppError['code'] is the closed classified-code union | | HttpError | Typed HTTP error class for services to throw | | isAbortError(error) | Guard for AbortError — use in catch blocks with shared-state side effects | | classifyError(error) | Maps raw errors → AppError |

React Integration

import { useInstance, useLocal, useSingleton } from 'mvc-kit/react';

| Hook | Description | |------|-------------| | useLocal(Class \| factory, ...args, deps?) | Component-scoped instance, auto-init/dispose, optional deps array to recreate | | useSingleton(Class, ...args) | Singleton resolution with auto-init and shared state | | useInstance(subscribable) | Subscribe to an existing instance (no lifecycle management) | | useModel(factory) | Model binding: { state, errors, valid, dirty, model } | | useFormModel(factory) | FormModel binding: adds gated visibleErrors + submitAttempted | | useField(model, key) | Single FormModel field subscription with surgical re-renders | | useEvent(source, event, handler) | Subscribe to an EventBus or ViewModel event | | useResolve(Class, ...args) | Resolve from Provider context or fall back to singleton() | | useTeardown(...Classes) | Teardown singletons on unmount |

Headless components (DataTable, CardList, InfiniteScroll) live in mvc-kit/react/ui — the hooks entry stays component-free.

DI & Testing — Provider and useResolve

useResolve(Class) resolves from the nearest Provider context, falling back to singleton() when none is present — so components stay injectable in tests and stories (wrap them in <Provider provide={[[ApiService, mockApi]]}>) without any test-only code. Full reference: Provider & useResolve.

See the DI mechanisms decision table for when to use singleton() vs Provider vs provideSingleton.

Documentation

  • BEST_PRACTICES.md — the authoritative patterns guide: rules, rationale, decision tables, quick-reference checklist
  • MIGRATION.md — v3 → v4 migration guide (import paths, removed APIs, new capabilities)
  • docs/extending.md — how to write a StorageAdapter, a plugin, or a framework binding
  • DESIGN.md — the v4 architecture contract

Every class and hook keeps a colocated reference doc next to its source:

Core (mvc-kit)

| Doc | Description | |-----|-------------| | ViewModel | State, computed getters, async tracking, typed events, lifecycle hooks | | Model | Validation, dirty tracking, commit/rollback for editable entities | | Collection | Reactive typed array, CRUD, optimistic updates, eviction & TTL | | PersistentCollection | Adapter-based storage persistence: hydration, write-behind, error policy | | Resource | Collection + async tracking with external Collection injection | | Controller | Stateless orchestrator for multi-ViewModel coordination | | Service | Non-reactive infrastructure adapters (HTTP, storage, SDKs) | | EventBus | Typed pub/sub for cross-cutting events | | Channel | Persistent connections (WebSocket, SSE) with auto-reconnect | | Trackable | Base class for custom reactive objects | | Singleton Registry | singleton(), provideSingleton(), teardown(), SSR dehydrate()/hydrate() | | Plugins | MvcKitPlugin hooks and installPlugin() |

App layer

| Doc | Description | |-----|-------------| | FormModel | Touched gating, submit reveal, server errors, validating flags | | Sorting · Pagination · Selection · Feed · Pending · ValueResource · PersistentValue | Composable UI-state helpers | | offline-kit | Durable offline-first mutation queue: createOfflineKit typed binder, Outbox, SyncController, SyncStatusViewModel, Connectivity |

React (mvc-kit/react, mvc-kit/react/ui)

| Doc | Description | |-----|-------------| | useLocal · useInstance · useSingleton | Reactive state bindings | | useModel · useFormModel & useField | Model/FormModel bindings | | useEvent · useTeardown | Events and lifecycle utilities | | Provider & useResolve | DI seam: context-provided instances with singleton() fallback | | SSR | Server rendering: getServerSnapshot, client-only onInit, data transfer | | DataTable · CardList · InfiniteScroll | Headless components |

Other bindings

| Doc | Description | |-----|-------------| | createLocal · createSingleton · reactive | Solid binding | | ViewModelElement | Web components binding |

Storage, testing, devtools

| Doc | Description | |-----|-------------| | Storage adapters | localStorageAdapter, sessionStorageAdapter, indexedDBAdapter, asyncStorageAdapter | | Testing kit | flushAsync, waitFor, waitForAsync, seedSingletons + the blessed unit pattern | | Devtools | loggerPlugin, inspectRegistry, attachDevtools |

Examples

examples/ holds two apps: a todo app — state, derivation, async, optimistic updates all in a shared TodosViewModel — bound to three shells, and an offline-first app (a durable mutation queue: offline send, sync-on-reconnect, retry/backoff; React shell, React Native-portable). From the package root:

npm run dev:react     # http://localhost:3000
npm run dev:solid     # http://localhost:3001
npm run dev:element   # http://localhost:3002

Dev Mode (__MVC_KIT_DEV__)

mvc-kit ships development-only safety checks (set-in-getter detection, ghost async ops, duplicate storage keys, reserved-key guards) behind the __MVC_KIT_DEV__ flag. It defaults to false — no bundler config required. Define it as true in your dev build (e.g. Vite define) to enable the checks; production builds dead-code-eliminate them.

License

MIT