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

@wieslawsoltes/reactiveweb

v0.2.0

Published

RxJS MVVM for JavaScript, HTML and React with ReactiveUI-style .NET APIs

Readme

ReactiveWeb

Reactive MVVM for JavaScript, TypeScript, HTML and React, powered by RxJS.

Interactive showcase · Releases and full source · API compatibility · Generation guide

ReactiveWeb brings familiar ReactiveObject, WhenAnyValue, ReactiveCommand, Interaction, WhenActivated, and RoutingState patterns to the web. The reference is ReactiveUI.Reactive, the System.Reactive-compatible distribution. The runtime uses the actual RxJS 7.8 observable, subject, subscription, scheduler and operator implementations. It does not provide a separate home-grown observable engine.

The API keeps PascalCase names for .NET application ports, offers selected JavaScript aliases, ships strong TypeScript declarations, and separates the core from HTML and optional React adapters. Reactive properties can be explicit accessors, standard TypeScript accessor decorators, schema-generated classes, or readable source produced at build time.

This is an independent MIT-licensed implementation. Version 0.2.0 implements the documented browser MVVM surface; it does not claim exhaustive API or behavioral equivalence to every ReactiveUI package and native platform. See the pinned upstream audit and detailed mapping.

Get the code and run the showcase

git clone https://github.com/wieslawsoltes/ReactiveWeb.git
cd ReactiveWeb
npm ci
npm run check
npm run dev

Open http://localhost:4173. The built showcase uses local relative assets and works under the GitHub Pages /ReactiveWeb/ path. It includes light/dark themes, responsive layouts, editable view models, live state, event streams and source examples for the main API families.

Install the library

Install the public npm package alongside RxJS:

npm install @wieslawsoltes/reactiveweb rxjs
# Only when using the /react adapter:
npm install react react-dom

Every versioned GitHub release also contains an npm-compatible .tgz:

npm install ./wieslawsoltes-reactiveweb-0.2.0.tgz rxjs

The release pipeline publishes the same verified package to npmjs and GitHub Packages. GitHub Packages consumers must configure @wieslawsoltes:registry=https://npm.pkg.github.com and authenticate according to GitHub's npm-registry requirements. Public npmjs installation requires no registry configuration. See publishing for release and credential setup.

| Entry point | Purpose | | --- | --- | | @wieslawsoltes/reactiveweb | Observable model, commands, lifetimes, routing, validation, collections, setup | | @wieslawsoltes/reactiveweb/html | DOM bindings, reactive custom elements and view hosts | | @wieslawsoltes/reactiveweb/react | React hooks, contexts and view hosts; React is optional | | @wieslawsoltes/reactiveweb/generation | Decorators and typed runtime schema generation | | @wieslawsoltes/reactiveweb/dynamic-data | Full DynamicData namespace and collection/change-set adapters | | @wieslawsoltes/reactiveweb/generator | Node-only source-generator API |

The package contains ESM, CommonJS, declarations, source maps and original source. Each module format preserves shared class identity between its entry points. Keep a consumer on one module format; mixing ESM and CommonJS copies creates separate module state, as with ordinary dual-format Node packages.

A complete view model in ordinary JavaScript

import {
  defineViewModel, reactiveProperty, WhenAnyValue,
  CompositeDisposable, DisposeWith
} from '@wieslawsoltes/reactiveweb';
import { Bind, OneWayBind, BindCommand } from '@wieslawsoltes/reactiveweb/html';

const ProfileViewModel = defineViewModel({
  properties: {
    FirstName: reactiveProperty('Alex'),
    LastName: reactiveProperty('Morgan')
  },
  computed: {
    FullName: {
      initialValue: '',
      source: vm => WhenAnyValue(vm, 'FirstName', 'LastName',
        (first, last) => `${first} ${last}`.trim())
    }
  },
  commands: {
    Save: {
      kind: 'task',
      canExecute: vm => WhenAnyValue(vm, 'FirstName', name => !!name.trim()),
      execute: async (vm, _input, signal) => {
        const response = await fetch('/api/profile', {
          method: 'POST', signal,
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ first: vm.FirstName, last: vm.LastName })
        });
        if (!response.ok) throw new Error(`Save failed: ${response.status}`);
        return response.json();
      }
    }
  }
});

const vm = new ProfileViewModel();
const lifetime = new CompositeDisposable();
lifetime.Add(vm);
DisposeWith(Bind(vm, 'FirstName', document.querySelector('#first'), 'value'), lifetime);
DisposeWith(OneWayBind(vm, 'FullName', document.querySelector('#name'), 'textContent'), lifetime);
DisposeWith(BindCommand(vm.Save, document.querySelector('#save')), lifetime);
lifetime.Add(vm.Save.ThrownExceptions.subscribe(error => console.error(error)));

// Call this when the owning view is removed:
// lifetime.Dispose();

/api/profile is your application endpoint. The showcase uses a local asynchronous operation so every example runs on static hosting without a server.

Class-based .NET-style usage

import { ReactiveObject, Reactive, ReactiveCommand, WhenAnyValue, ToProperty }
  from '@wieslawsoltes/reactiveweb';

class PersonViewModel extends ReactiveObject {
  @Reactive accessor FirstName = 'Alex';
  @Reactive accessor LastName = 'Morgan';

  private readonly fullName = ToProperty(
    WhenAnyValue(this, 'FirstName', 'LastName', (a: string, b: string) => `${a} ${b}`),
    this, 'FullName', { initialValue: '' }
  );
  get FullName(): string { return this.fullName.Value; }

  readonly Reset = ReactiveCommand.Create(() => {
    const batch = this.DelayChangeNotifications();
    try { this.FirstName = 'Alex'; this.LastName = 'Morgan'; }
    finally { batch.Dispose(); }
  });

  override Dispose() {
    this.Reset.Dispose();
    this.fullName.Dispose();
    super.Dispose();
  }
}

Use standard TypeScript 5+ decorators with accessor; legacy experimentalDecorators is a different decorator system. For ordinary JavaScript, write get Name() { return this.GetValue('Name', ''); } and set Name(value) { this.RaiseAndSetIfChanged('Name', value); }, or use the schema factory above. Property reads/writes use normal accessors; no Proxy is required in the generated property hot path.

HTML custom elements

import { ReactiveElement } from '@wieslawsoltes/reactiveweb/html';

class PersonView extends ReactiveElement<PersonViewModel> {
  constructor() {
    super();
    this.attachShadow({ mode: 'open' }).innerHTML = `
      <label>First name <input data-rx-bind="FirstName"></label>
      <strong data-rx-text="FullName"></strong>
      <button data-rx-command="Reset">Reset</button>
    `;
  }
}
customElements.define('person-view', PersonView);
const element = document.createElement('person-view') as PersonView;
element.ViewModel = new PersonViewModel();
document.body.append(element);

Bindings activate on connection, stop on disconnection, and rebind on view-model replacement. A view does not automatically own the lifetime of its assigned model; dispose models according to your application's ownership. Declarative bindings resolve member paths, never evaluate arbitrary JavaScript strings. Text binding uses textContent. See HTML, React, validation and converter APIs.

React

import { useReactiveObject, useReactiveCommand, useWhenActivated }
  from '@wieslawsoltes/reactiveweb/react';

function Counter({ viewModel }) {
  const vm = useReactiveObject(viewModel);
  const increment = useReactiveCommand(vm.Increment);
  useWhenActivated(vm); // vm provides Activator: ViewModelActivator
  return <button disabled={!increment.canExecute}
    onClick={() => increment.execute(undefined)}>
    Count: {vm.Count}
  </button>;
}

useObservable and useReactiveObject integrate with useSyncExternalStore. Server rendering does not start subscriptions. Activation effects own disposable leases and are tested under React StrictMode. Vue, Svelte, Angular, Lit and other frameworks can consume the core RxJS streams or the custom elements; dedicated framework-specific adapters for those frameworks are not included.

Build-time property generation

{
  "className": "CounterViewModel",
  "properties": {
    "Count": { "type": "number", "initial": 0, "validate": { "minimum": 0 } }
  }
}
npx reactiveweb-generate counter.schema.json --out CounterViewModel.ts
# Use .js output for an ESM JavaScript class.

Generated output is readable source with explicit accessors. Named imports supply computed-property factories and command bodies. Invalid schemas and member collisions fail early; overwriting existing output requires --force. See generation and compilable examples.

Available feature families

| Area | APIs and behavior | | --- | --- | | Objects and observation | Changing, Changed, RaiseAndSetIfChanged, suppression, delayed batching, nested WhenAnyValue/WhenAny/WhenAnyObservable, property providers | | Observable properties | ToProperty, deferred ObservableAsPropertyHelper, ReactiveProperty, scheduler selection, error and validation streams | | Commands | Sync, Promise/AbortSignal, Observable and combined commands, cold execution, concurrency accounting, exception streams, gated InvokeCommand | | Lifetimes | Disposable, composite, serial, single-assignment, reference counting, ViewModelActivator, WhenActivated | | Interactions | Typed input/output, newest-first handlers, async/Observable handlers, cancellation and unhandled errors | | Routing | Navigation stack, navigate/back/reset commands, view locator contracts, HTML and React hosts, enter/leave resource scopes | | Validation | Property and stream validation, async obsolete-result cancellation, aggregate pending/errors, command gating | | Collections | DynamicDataWeb list/cache pipelines, property refresh, incremental collection binding, filtering/sorting/paging, HTML/React views and per-object resource ownership | | Services and messages | Factory/constant/lazy-singleton registrations, contracts, reversible registrations, live/latest typed message channels | | Persistence | Suspension lifecycle, memory/localStorage drivers, JSON migration hooks, serial saves, AutoPersist/AutoPersistCollection | | Setup and conversion | RxAppBuilder, modular facades, property-provider registration, affinity-based converters, binding hooks | | Scheduler/operator helpers | ScheduledSubject, WaitForDispatcherScheduler, switching subscriptions, logged catch and observation operators | | Generation | Standard accessor decorators, runtime schema classes, TypeScript/JavaScript CLI source generation |

Read core API usage, application setup, observable helpers, and the compatibility matrix for exact contracts and adaptations.

Buildless browser distribution

Extract reactiveweb-browser.tar.gz from a release and serve the entire directory, including browser-chunks/ and third-party/:

<script type="module">
  import { ReactiveObject, rxjs } from './reactiveweb.browser.js';
  import { Bind } from './reactiveweb-html.browser.js';
  const vm = new ReactiveObject({ Name: 'Alex' });
  Bind(vm, 'Name', document.querySelector('input'), 'value');
  vm.WhenAnyValue('Name').pipe(rxjs.map(name => name.toUpperCase()))
    .subscribe(console.log);
</script>

The standalone entries share generated chunks to preserve class identity. RxJS is bundled into that distribution, and its namespace is exported as rxjs. The npm ESM/CommonJS entries keep RxJS external for normal application bundlers.

Verification and releases

npm run check            # clean build, strict example types, behavioral tests, demo
npm run test:package     # actual npm tarball in an isolated consumer
npx playwright install chromium
npm run test:browser     # complete showcase interactions, desktop/mobile/dark
npm run benchmark       # measured local observations, not cross-framework claims
npm pack                # publishable .tgz

CI validates Node 22 and 24, compiles and executes generated source, checks installed ESM/CommonJS consumers, exercises Chromium, and retains visual artifacts. Successful main builds deploy GitHub Pages and publish a new package version to GitHub Packages and GitHub Releases, then call the npm publication workflow with provenance. The workflow verifies registry integrity and tests the downloaded npm tarball. Increment the version and update release notes for a new release; existing release assets are left unchanged. Manual publication of an existing release remains available.

Platform and compatibility boundaries

JavaScript cannot preserve CLR binary interfaces, C# overload resolution, expression-tree semantics, Roslyn diagnostics, native control hierarchies, or operating-system suspension guarantees. Explicit tokens replace erased runtime generic types, member paths replace expression trees, and web lifecycle adapters replace native UI integrations. DynamicData collection pipelines are provided by the published @wieslawsoltes/dynamicdataweb dependency and integrated with ReactiveWeb models, collections, persistence, HTML and React. Its own overload and platform adaptations remain documented in the DynamicData integration guide.

The suite verifies the cases named in its tests. It is not an exhaustive upstream differential conformance suite, production-scale soak test, or all-browser certification. The compatibility matrix records the remaining method/overload and platform boundaries, including intentional behavioral choices.

License

MIT. Upstream reference and dependency attributions are in NOTICE.