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

watch-state

v3.6.2

Published

CANT inc. state management system.

Readme

watch-state is a lightweight, high-performance reactive state engine designed to power UI frameworks — or replace them.

  • Fast — One of the fastest reactive libraries (see benchmarks)
  • Light — Less than 1 KB minzip
  • Zero-dependency — No external packages required
  • Code splitting by design — Decentralized state architecture, each page loads only the states it uses
  • Auto-subscription — Dependencies tracked automatically, no manual subscriptions
  • Dynamic subscriptions — Conditional watchers auto-subscribe/unsubscribe based on reactive conditions
  • Type-safe — Full TypeScript support with type inference
  • Memory-safe — Automatic cleanup on destroy
  • Lazy computation — Compute executes only when accessed
  • No Proxy — Supports old browsers (Firefox 45+, Safari 9+)
  • Framework-agnostic — Business logic lives outside components, reusable across any framework or vanilla JS

Use it as the core state layer in your own framework, embed it in React components, or build a full UI — no JSX, no virtual DOM, no framework required.

Born while working on @innet/dom.

stars watchers

Browser Support

Desktop

| Firefox | Chrome | Safari | Opera | Edge | |:-------:|:------:|:------:|:-----:|:----:| | 45+ | 49+ | 9+ | 36+ | 13+ |

Mobile

| Firefox | Chrome | Safari | Opera | |:-------:|:------:|:------:|:-----:| | 87+ | 90+ | 9+ | 62+ |

You can transpile the code to support browsers older than listed above, but performance will decrease.

Index

[ Install ]
[ Usage ] Simple exampleExample Vanilla JSExample ReactExample Innet
[ Watch ] Force update of WatchDestroy WatchDeep/Nested Watchers
[ State ] Get or Set valueState.setForce update of StateRaw valueInitial valueReset value
[ Compute ] Lazy computationForce update of ComputeDestroy Compute
[ Utils ] onDestroycallEventcreateEventunwatch
[ Typescript ] State type inferenceCompute type inference
[ Performance ]

Install

🏠︎ / Install

npm

npm i watch-state

yarn

yarn add watch-state

html

<script src="https://cdn.jsdelivr.net/npm/watch-state"></script>

minified on GitHub

Usage

🏠︎ / Usage

Simple exampleExample Vanilla JSExample ReactExample Innet

The library is based on the core concepts of Observable (something that can be observed) and Observer (something that can observe). On top of these concepts, the core classes State, Compute, and Watch are built according to the following scheme:

   ┌────────────┐ ┌─────────────┐
   │ Observable │ │  Observer   │
   │ (abstract) │ │ (interface) │
   └──────┬─────┘ └──────┬──────┘  
     ┌────┴─────┐ ┌──────┴───┐
┌────┴────┐ ┌───┴─┴───┐ ┌────┴────┐
│  State  │ │ Compute │ │  Watch  │
└─────────┘ └─────────┘ └─────────┘

Simple example

🏠︎ / Usage / Simple example

You can create an instance of State and watch its value.

import { Watch, State } from 'watch-state'

const count = new State(0)

new Watch(() => console.log(count.value))
// logs: 0

count.value++
// logs: 1

count.value++
// logs: 2

Example Vanilla JS

🏠︎ / Usage / Example Vanilla JS

Simple reactive state without build tools or framework dependencies.

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Counter</title>
    <script src="https://cdn.jsdelivr.net/npm/watch-state"></script>
    <script type="module">
      const { State, Watch } = WatchState

      const count = new State(0)
      const button = document.createElement('button');

      document.body.appendChild(button);

      new Watch(() => {
        button.innerText = count.value
      })

      button.addEventListener('click', () => {
        count.value++
      })
    </script>
</head>
<body>
</body>
</html>

Example React

🏠︎ / Usage / Example React

@watch-state/react provides hooks that automatically subscribe React components to state changes and re-renders only when needed.

import { State } from 'watch-state'
import { useObservable } from '@watch-state/react'

const $count = new State(0)

const increase = () => {
  $count.value++
}

export function CountButton () {
  const count = useObservable($count)

  return <button onClick={increase}>{count}</button>
}

Example Innet

🏠︎ / Usage / Example Innet

@innet/dom automatically watches accessed states and updates only changed DOM contentno full re-renders.

import { State } from 'watch-state'

const count = new State(0)

const increase = () => {
  count.value++
}

export function CountButton () {
  return <button onClick={increase}>{count}</button>
}

Watch

🏠︎ / Watch

Force update of WatchDestroy WatchDeep/Nested watchers

Watch accepts a reaction as its first argument and executes it when any accessed state changes. State accessed inside a reaction is auto-subscribed — no manual registration needed.

const state = new State(0)

const reaction = () => {
 console.log(state.value)
 // auto-subscribes to state
}

new Watch(reaction)
// logs: 0

state.value = 1 // triggers reaction
// logs: 1

Force update of Watch

🏠︎ / Watch / Force update of Watch

You can run a reaction even when its states are not updated.

const count = new State(0)

const watcher = new Watch(() => {
  console.log(count.value)
})
// logs: 0

watcher.update()
// logs: 0

Destroy Watch

🏠︎ / Watch / Destroy Watch

You can stop watching by destroy method of Watch.

const count = new State(0)

const watcher = new Watch(() => {
  console.log(count.value)
})
// logs: 0

count.value++
// logs: 1

watcher.destroy()

count.value++
// nothing happens

Deep/Nested Watchers

🏠︎ / Watch / Deep/Nested Watchers

Each Watch independently tracks only states accessed within its reaction. Nested watchers created inside parent watchers form a dependency tree with separate reactivity.

const watching = new State(true)
const state = new State(0)

new Watch(() => {
  console.log('Root Render')

  if (watching.value) {
    new Watch(() => {
      console.log(`Deep Render: ${state.value}`)
    })
  }
})
// logs: Root Render, Deep Render: 0

state.value++
// logs: Deep Render: 1  (only deep watcher reacts)

watching.value = false
// logs: Root Render     (deep watcher destroyed)

state.value++
// nothing happens       (no active deep watcher)

State

🏠︎ / State

Get or Set valueState.setForce update of StateRaw valueInitial valueReset value

Reactive primitive that holds a value and automatically notifies all subscribers when it changes.

Get or Set value

🏠︎ / State / Get or Set value

Reading .value inside reaction auto-subscribes to changes. Writing .value triggers all reactions.

const count = new State(0)

new Watch(() => console.log(count.value))
// auto-subscribes and logs 0

count.value++ // triggers: logs 1

State.set

🏠︎ / State / State.set

State.set mirrors the behavior of the value setter but returns void. It is useful as a shorthand in arrow functions: () => state.set(nextValue) instead of () => { state.value = nextValue }.

Note: state.set cannot be used as a standalone function; const set = state.set is not supported.

const count = new State(0)

// Subscribing
new Watch(() => console.log(count.value))
// logs: 0

count.set(1)
// logs: 1

Force update of State

🏠︎ / State / Force update of State

You can run reactions of a state with update method.

// Create state
const log = new State<number[]>([])

// Subscribe to changes
new Watch(() => console.log(log.value)) // logs: []

// Modify the array
log.value.push(1) // no logs
log.value.push(2) // no logs

// Update value
log.update() // logs: [1, 2]

Raw value

🏠︎ / State / Raw value

raw returns the current value but does not subscribe to changes — unlike value.

const foo = new State(0)
const bar = new State(0)

 new Watch(() => console.log(foo.value, bar.raw))
// logs: 0, 0

foo.value++ // logs: 1, 0
bar.value++ // no logs
foo.value++ // logs: 2, 1

Initial value

🏠︎ / State / Initial value

initial stores the initial value passed to the constructor. Useful for checking if the state has been modified by comparing state.initial === state.raw.

const count = new State(0)

console.log(count.initial)
// logs: 0

count.value = 5
console.log(count.initial === count.raw)
// logs: false

count.reset()
console.log(count.initial === count.raw)
// logs: true

Reset value

🏠︎ / State / Reset value

reset() restores the state to its initial value. Triggers watchers only if the current value differs from the initial value.

const count = new State(0)

new Watch(() => console.log(count.value))
// logs: 0

count.value = 5
// logs: 5

count.reset()
// logs: 0

count.reset()
// no logs (value already 0)

Compute

🏠︎ / Compute

Lazy computationForce update of ComputeDestroy Compute

Compute accepts a reaction as its first argument and represents a reactive value returned by the reaction. It creates a derived state that automatically tracks dependencies and caches the result.

Lazy computation

🏠︎ / Compute / Lazy computation

Compute doesn't execute immediately — waits for .value access.
Dependencies (State.value reads inside reaction) auto-subscribe like Watch.

const name = new State('Foo')
const surname = new State('Bar')

const fullName = new Compute(() => (
  `${name.value} ${surname.value[0]}` // auto-subscribes to name+surname
))
// NO COMPUTATION YET — lazy!

new Watch(() => {
  console.log(fullName.value) // FIRST ACCESS → computes!
})
// logs: 'Foo B'

surname.value = 'Baz' // surname[0] still "B"
// nothing happens

surname.value = 'Quux' // surname[0] = "Q"
// logs: 'Foo Q'

Force update of Compute

🏠︎ / Compute / Force update of Compute

You can run a reaction of a compute with update method.

const items = new State([])

const itemCount = new Compute(() => {
  console.log('Recomputing length...')
  return items.value.length
})

new Watch(() => console.log('Watcher sees:', itemCount.value))
// logs: Recomputing length...
// logs: Watcher sees: 0

items.value.push('apple')
// Array reference SAME → NO recompute!

itemCount.update()
// logs: Recomputing length...
// logs: Watcher sees: 1

Destroy Compute

🏠︎ / Compute / Destroy Compute

You can stop watching by destroy method of Compute.

const user = new State({ name: 'Alice', age: 30 })

const userName = new Compute(() => {
  console.log('Computing')
  return user.value.name.toUpperCase()
})

new Watch(() => console.log(userName.value))
// logs: Computing
// logs: ALICE

user.value = { name: 'Mike', age: 32 }
// logs: Computing
// logs: MIKE

userName.destroy()

user.value = { name: 'Bob', age: 31 }
// nothing happens — fully disconnected!

Utils

🏠︎ / Utils

onDestroycallEventcreateEventunwatch

onDestroy

🏠︎ / Utils / onDestroy

You can subscribe on destroy or update of watcher

const count = new State(0)

const watcher = new Watch(() => {
  console.log('count', count.value)
  // the order does not matter
  onDestroy(() => console.log('destructor'))
})
// logs: 'count', 0

count.value++
// logs: 'destructor'
// logs: 'count', 1

watcher.destroy()
// logs: 'destructor'

count.value++
// nothing happens

callEvent

🏠︎ / Utils / callEvent

You can immediately execute a reactive effect with callEvent.

callEvent batches all state updates inside the callback and triggers watchers only once at the end.

const a = new State(0)
const b = new State(0)

new Watch(() => {
 console.log(a.value, b.value)
})
// logs: 0, 0

a.value = 1
// logs: 1, 0

b.value = 1
// logs: 1, 1

callEvent(() => {
 a.value = 2
 b.value = 2
})
// logs: 2, 2

callEvent returns exactly what your callback returns — TypeScript infers the correct type automatically.

const count = new State(0)

new Watch(() => console.log(count.value))
// logs: 0

const prev = callEvent(() => count.value++)
// logs: 1

console.log(prev)
// logs: 0

createEvent

🏠︎ / Utils / createEvent

You can create a reusable event function with createEvent.

Like callEvent, it batches state updates and triggers watchers only once after execution.

import { State, createEvent } from 'watch-state'

const count = new State(0)
const increase = createEvent(() => count.value++)

new Watch(() => console.log(count.value))
// logs: 0

increase()
// logs: 1

increase()
// logs: 2

unwatch

🏠︎ / Utils / unwatch

You can disable automatic state subscriptions with unwatch.

import { State, Watch, unwatch } from 'watch-state'

const count = new State(0)

new Watch(() => {
  console.log(unwatch(() => count.value++))
})
// logs: 0

count.value++
// logs: 1

console.log(count.value)
// logs: 2

Typescript

🏠︎ / Typescript

State type inference

🏠︎ / Typescript / State type inference

Type inference from initial value:
Type is automatically inferred from the initial value passed to the constructor — no generic needed.

const count = new State(0) // State<number>

count.value = 'str' // error: number expected

Without initial value:
When using a generic without an initial value, initial is undefined, which may conflict with strict types.

const value = new State<string>()
// value.initial is undefined (not string)

// To allow undefined in type:
const maybe = new State<string | undefined>()

State as a type annotation:
Without a generic, State defaults to State<unknown>, which accepts any value type.

const foo: State = new State(0)

foo.value = 'str' // ok (unknown allows any)
foo.value = true  // ok

// Specify generic for type safety:
const bar: State<number> = new State(0)

bar.value = 'str' // error

Compute type inference

🏠︎ / Typescript / Compute type inference

Type inferred from function return:
Type is automatically inferred from the function's return value — no generic needed.

const fullName = new Compute(() => `${firstName.value} ${lastName.value}`)
// Compute<string> — no generic needed

const length = new Compute(() => items.value.length)
// Compute<number>

Explicit generic (usually not needed):
Explicit generics are rarely needed since types are inferred. Use only when you want to enforce a specific type.

new Compute<string>(() => false) // error: boolean not assignable to string

Destroyed Compute and undefined:
Compute.value is typed as the function return type, but if you access .value after destroy() (before any computation ran), it returns undefined.

const computed = new Compute(() => expensiveCalculation())

computed.destroy()
console.log(computed.value) // undefined (but typed as return type)

This is intentional — accessing destroyed observers is rare and shouldn't require undefined checks in normal code.

Performance

🏠︎ / Performance

You can check a performance test with MobX, Effector, Storeon, Nano Stores, Mazzard and Redux. Clone the repo, install packages and run this command

npm run speed

Links

You can find more tools here

Issues

If you find a bug or have a suggestion, please file an issue on GitHub

issues