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

@slimr/observable

v0.0.6

Published

Tiny, framework-agnostic pub/sub state — like signals, without tying you to a framework.

Downloads

940

Readme

@slimr/observable

Tiny, framework-agnostic pub/sub state — like signals, without tying you to a framework.

| Entry | Import | Use when | |-------|--------|----------| | Core | @slimr/observable | Shared state, libraries, non-React code | | React | @slimr/observable/react | Hooks, ObservableR, component-local state |

npm install @slimr/observable
# React: peer deps `react` + `react-dom`

Naming convention

Observable instances are named with a $ suffix (e.g. count$, user$). This signals reactive state at a glance and is consistent across all @slimr packages.


Core (Observable)

Each instance has a unique name (registered on globalThis.observables[name] for debugging), a current value via val / set, and subscribe listeners.

Updates run through set (or val = …). Subscribers fire only when the new value is not deep-equal to the previous one. Object values returned from val are frozen — mutate by replacing the root value, not nested fields in place.

import { Observable } from "@slimr/observable"

const count$ = new Observable("count$", 0)

const unsub = count$.subscribe((n) => console.log(n))
count$.val = 1
await count$.set((n) => n + 1)
unsub()

Subscribing to a slice (select)

For compound values, pass a selector as the second argument. The callback receives the slice and runs only when that slice changes (deep equality):

const state$ = new Observable("state$", { bar: 2, man: 3 })

state$.subscribe(
  (man) => console.log("man:", man),
  (s) => s.man,
)

await state$.set((s) => ({ ...s, man: 4 }))   // logs 4
await state$.set((s) => ({ ...s, bar: 99 }))  // silent

Prefer one observable per concern when you can (pending$, user$). Use select when several fields must live in one atomic snapshot.

Manual notification (notify)

When you mutate internal state in place (e.g. a nested property on an object) and need to notify subscribers, call notify() to force-fire all subscribers with the current value, bypassing the deep-equality check:

const list$ = new Observable("list$", [1, 2, 3])
list$.subscribe((v) => console.log(v.length))
list$.val.push(4)      // push works despite shallow freeze on Array
await list$.notify()   // logs 4

This is an escape hatch from the recommended pattern of replacing the root value. Prefer set when you can.


React

ObservableR — Include .use() hooks

Subclass of Observable with .use() as sugar over useObservable (supports select and getServerSnapshot):

import { ObservableR } from "@slimr/observable/react"

const gate$ = new ObservableR("gate$", false)

function Gate() {
  const pending = gate$.use()
  if (pending) return <div>Loading…</div>
  return <App />
}

Slice-only re-renders — same select as subscribe:

function ManOnly({ state$ }: { state$: ObservableR<{ bar: number; man: number }> }) {
  const man = state$.use({ select: (s) => s.man })
  return <span>{man}</span>
}

useObservable — subscribe to a shared observable

Use for generic (aka non-react) observables, or you need to pass the hook options dynamically:

import type { Observable } from "@slimr/observable"
import { useObservable } from "@slimr/observable/react"

function Gate({ pending$ }: { pending$: Observable<boolean> }) {
  const pending = useObservable(pending$)
  if (pending) return <div>Loading…</div>
  return <App />
}

Slice-only re-renders — same select as subscribe:

function ManOnly({ state$ }: { state$: Observable<{ bar: number; man: number }> }) {
  const man = useObservable(state$, { select: (s) => s.man })
  return <span>{man}</span>
}

For libraries publishing state, stick to base Observable + consumers call useObservable($) or new ObservableR(name, init) locally.

useLocalObservable — component-local state

Not pub/sub. A mutable handle.value that triggers re-renders on assignment (including ++):

import { useLocalObservable } from "@slimr/observable/react"

function Counter() {
  const count$ = useLocalObservable(0)
  return (
    <button type="button" onClick={() => count$.value++}>
      {count$.value}
    </button>
  )
}

SSR (getServerSnapshot)

When the server snapshot should differ from the client’s first paint (e.g. always false for a loading gate):

const pending = useObservable(syncPending$, {
  getServerSnapshot: () => false,
})

// ObservableR
const pending2 = gate$.use({ getServerSnapshot: () => false })

getServerSnapshot returns the full observable value; select is applied when reading the hook result.


Cheat sheet

| Goal | API | |------|-----| | Shared mutable cell | new Observable(name$, initial) | | Notify on change | $.subscribe(cb) | | Notify when one field changes | $.subscribe(cb, select) | | Read in React | $.use() (preferred) or useObservable($) | | Re-render on one field | $.use({ select }) (preferred) or useObservable($, { select }) | | Local component state | useLocalObservable(initial) | | SSR-safe first paint | { getServerSnapshot: () => … } |