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

@remodulo/view-model

v0.1.1

Published

The remodulo view model base class: sealed lifecycle hooks, mount-scoped disposers and abort signal. Zero dependencies, reactivity-agnostic.

Readme

@remodulo/view-model

The reference lifecycle citizen for remodulo.

A base class that owns disposers and an AbortSignal, releases them at the right moment, and seals its own lifecycle entry points so a subclass cannot silently disable the teardown. Reactivity-agnostic and host-agnostic: it knows nothing about MobX, and nothing about React.

⚠️ Experimental / internal use.

Primarily intended for personal and internal use. It may change, break, or be restructured at any time. Don't rely on it for public projects unless you're prepared to maintain your own fork.

Install

npm install @remodulo/view-model

Zero dependencies and zero peers — not an accident, the point. The class is plain TypeScript over AbortController and an array, so it costs a consumer nothing to adopt and cannot drag a second copy of anything into their graph. The declarations gate pins it: the emitted ViewModel.d.ts contains no import at all.

Example

import { ViewModel } from "@remodulo/view-model"

class SearchStore extends ViewModel {
    results: string[] = []

    protected onMount(): void {
        this.track(subscribe(this.query, (results) => (this.results = results)))
    }

    protected async onDestroy(): Promise<void> {
        await this.flushPendingWrites()
    }

    private async query(term: string): Promise<string[]> {
        const response = await fetch(`/search?q=${term}`, { signal: this.signal() })
        return response.json()
    }
}

The four hooks

onInit(), onMount(), onUnmount(), onDestroy() — all optional, all protected, no super call. Declare only the ones you need, as a method or an arrow field. onDestroy is the only one that may return a promise; it is awaited.

track() and signal() are mount-scoped

track(disposer) registers cleanup and returns the disposer unchanged, so it wraps a subscription inline. signal() lazily mints an AbortController and hands you its signal.

Both release at unmount, in reverse registration order, each disposer in its own try/catch — what a mount acquires, the matching unmount lets go, so a remount starts clean instead of stacking a second subscription on the first. The controller is aborted after the disposer flush and then dropped, so the next mount gets a fresh signal.

Destroy is the backstop, not the scope. A module that is created and destroyed without ever mounting would otherwise leak whatever its constructor tracked, so onDestroy runs the same release afterwards. Cleanup that genuinely belongs to the object's whole life is just the body of onDestroy — there is no second registry for it.

The seal, in three layers

The four onModule* hooks are the module lifecycle's entry points and the base owns all four. A subclass that redefined one would drop the teardown the base runs after it, so the base refuses the redefinition rather than trying to wrap it:

  1. Compile time. The hooks are private, so they are emitted as private onModuleInit; and its three siblings. A subclass that redeclares one — method or arrow field — fails to compile before it can ever throw.
  2. Construction. The constructor walks the prototype chain up to ViewModel.prototype and throws on any own onModule* descriptor, naming the replacement: ViewModel seals onModuleMount() - override onMount() instead. A grandchild is caught the same as a child.
  3. The property itself. Each hook is then pinned as a non-writable, non-configurable own property of the instance. That closes the route the prototype scan cannot see — a class field initializer runs after the base constructor, so it gets a TypeError instead. Assignment and defineProperty after construction throw for the same reason.

The hooks are non-enumerable, so the instance still reads as plain state from the outside. Reflect.ownKeys does see them, which is the trap for any annotation pass that walks own keys — @remodulo/mobx's makeInheritedAutoObservable carries a skip for exactly that.

Honesty about the coupling

Nothing here imports @remodulo/react. The onModule* names are adopted structurally — the module lifecycle looks them up at runtime and never names their types — which is what lets this package stay dependency-free while still being a first-class participant. The flip side is that the contract is a naming convention rather than a compiler-checked interface, and the two packages' suites are what keep it true.

Documentation

License

MIT