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

@lickle/state

v0.0.1

Published

A tiny, type-safe reactive state library

Readme

@lickle/state

A tiny, type-safe reactive state library. State lives in atoms; derived values and effects compose from them.

Build Status Build Size Version Downloads

Install

npm install @lickle/state

Quick Start

import { atom, derive, effect } from '@lickle/state/atom'

const count = atom(0)
const doubled = derive([count], (n) => n * 2)

const stop = effect(count, () => console.log('count:', count.get()))
// logs "count: 0" immediately
count.set(1) // logs "count: 1"
stop()

Core Concept

A reactive atom holds a value, lets you read it with get, write it with set, and subscribe to changes with sub. All other primitives compose from this interface.

type ReadableAtom<T> = {
  get: () => T
  sub: (callback: () => void) => () => void // returns unsub
}

type Atom<T> = ReadableAtom<T> & {
  lc: number  // current subscriber count
  set: (value: T) => void
}

Atoms

import { atom } from '@lickle/state/atom'

const count = atom(0)   // writable atom
count.get()             // 0
count.set(1)            // update — notifies all subscribers synchronously

const unsub = count.sub(() => console.log(count.get()))
count.set(2)  // logs 2
unsub()

Pass a custom equality function to control when subscribers are notified. The default is ===; pass () => false to always notify (useful for mutable objects):

const pos = atom({ x: 0, y: 0 }, (a, b) => a.x === b.x && a.y === b.y)
pos.set({ x: 0, y: 0 }) // suppressed — same coordinates
pos.set({ x: 1, y: 0 }) // fires

update applies a transform in a single call:

update(count, (n) => n + 1)

readonly hides set, useful for exposing internal atoms to consumers:

const _count = atom(0)
export const count = readonly(_count) // no .set on the public surface

Derived State

derive computes a value from one or more source atoms. It subscribes lazily (only while it has its own subscribers) and skips notifications when the computed value is unchanged.

import { atom, derive, select } from '@lickle/state/atom'

const a = atom(2)
const b = atom(3)
const sum = derive([a, b], (x, y) => x + y)
sum.get() // 5
a.set(10)
sum.get() // 13

select is the single-source shorthand with an optional equality check:

const user = atom({ name: 'Alice', age: 30 })
const name = select(user, (u) => u.name)
name.get() // 'Alice'

// Only notify when the array contents change:
const ids = select(store, (s) => s.ids, (a, b) => a.join() === b.join())

Async Derived State

deriveAsync wraps an async function, exposing its state as a { loading, error, value } atom. Sources are subscribed lazily; stale in-flight requests are discarded when sources change.

import { atom, deriveAsync } from '@lickle/state/atom'

const userId = atom('u1')
const profile = deriveAsync([userId], async (id) => fetchUser(id))

profile.get() // { loading: true, error: null, value: null }

profile.sub(() => {
  const { loading, error, value } = profile.get()
  if (!loading && !error) render(value)
})

userId.set('u2') // cancels the previous fetch, starts a new one

Effects

effect subscribes to a reactive source, running the callback immediately and again on every change. The callback may return a cleanup function that runs before each re-fire and on final dispose:

import { atom, effect } from '@lickle/state/atom'

const count = atom(0)
const stop = effect(count, () => {
  const id = setInterval(() => count.set(count.get() + 1), 100)
  return () => clearInterval(id)
})
stop()

watch is like effect but skips the initial run — useful when you only care about future changes:

const stop = watch(count, () => console.log('changed:', count.get()))
count.set(1) // logs "changed: 1"
stop()

onMount runs a setup function the first time a reactive gains a subscriber, calling its returned cleanup when the last subscriber leaves:

const clock = atom(Date.now())

onMount(clock, () => {
  const id = setInterval(() => clock.set(Date.now()), 1000)
  return () => clearInterval(id)
})
// interval only runs while clock has subscribers

When composing with batch, apply batch first: onMount(batch(atom), callback).


Keyed Atoms

keyed is a reactive key/value store with per-key and global subscriptions.

import { keyed } from '@lickle/state/atom'

type Session = { userId: string; theme: 'light' | 'dark' }
const session = keyed<Session>()

// Per-key subscription — typed to that key's value:
const stopTheme = session.sub('theme', (v) => console.log('theme:', v))

// Global subscription — fires on any change:
const stopAll = session.sub((key, value) => console.log(key, '→', value))

session.set('theme', 'dark') // fires both
session.delete('userId')     // fires global with undefined
stopTheme(); stopAll()

Batching

batch wraps an atom so that rapid set calls coalesce into a single subscriber notification. It mutates and returns the atom.

import { atom, batch } from '@lickle/state/atom'

const a = atom(0)
batch(a) // default: microtask scheduling

a.sub(() => console.log(a.get()))
a.set(1)
a.set(2)
a.set(3)
// callback fires once (value: 3) after the current microtask
batch.timeout(100, a) // flush after 100 ms

// custom Scheduler: (flush: () => void) => cancelFn
batch(a, (cb) => {
  const id = requestAnimationFrame(cb)
  return () => cancelAnimationFrame(id)
})

Persistence

persist connects a writable atom to a storage backend: it hydrates the atom from storage on init, then writes every set back. Works with both sync and async backends.

import { atom, persist } from '@lickle/state/atom'
import { storage } from '@lickle/state/storage'

const count = persist('count', atom(0), storage('local'))
count.set(5) // persisted to localStorage
// on next page load, persist(...) hydrates back to 5

Storage

import { storage, memory, prefix, serialized, indexedDB } from '@lickle/state/storage'

storage builds a backend by name or passes an existing one through:

storage('local')     // localStorage, JSON-serialized
storage('session')   // sessionStorage, JSON-serialized
storage('indexedDB') // IndexedDB (returns AsyncStorage)
storage(memory())    // pass-through

memory is an in-memory store backed by a Map — useful in tests and as a fallback:

const store = memory<{ count: number }>()
store.set('count', 1)
store.get('count') // 1

prefix namespaces keys. Call it curried to compose, or data-first to apply immediately:

const ns = prefix(memory(), 'app:')
ns.set('count', '1') // stored as 'app:count'

// curried, for use with pipe:
const withPrefix = prefix<Record<string, string>>('app:')

serialized adapts a string-only store (e.g. localStorage) to hold structured values:

const store = serialized<{ count: number }>(memory())
store.set('count', 42)
store.get('count') // 42

// curried:
const withSerialization = serialized<{ count: number }>()

indexedDB stores values in IndexedDB, returning an AsyncStorage:

const db = indexedDB<string, number>('mydb', 'keyval')
await db.set('score', 100)
await db.get('score')    // 100
await db.keys()          // ['score']
await db.clear()

Backends compose:

const store = serialized(prefix(memory(), 'app:'))
persist('count', atom(0), store)

React

import { useStore, useSignal } from '@lickle/state/react'

useStore subscribes a component to a ReadableAtom, with an optional selector and equality function to limit re-renders:

import { atom } from '@lickle/state/atom'
import { useStore } from '@lickle/state/react'

const count = atom(0)

function Counter() {
  const n = useStore(count)
  return <button onClick={() => count.set(n + 1)}>{n}</button>
}

// Selector — re-renders only when the name changes:
const user = atom({ name: 'Alice', age: 30 })
function Name() {
  const name = useStore(user, (u) => u.name)
  return <span>{name}</span>
}

useSignal subscribes to an auto-tracked signal getter, re-rendering whenever any signal it reads changes:

import { signal, memo } from '@lickle/state/signal'
import { useSignal } from '@lickle/state/react'

const [count, setCount] = signal(0)
const doubled = memo(() => count() * 2)

function Counter() {
  const n = useSignal(doubled)
  return <button onClick={() => setCount((x) => x + 1)}>{n}</button>
}

Signals

The @lickle/state/signal sub-path exports auto-tracked reactive signals — a push-pull model where reading a value inside an effect or memo automatically subscribes it.

import { signal, effect, memo, batch, untrack } from '@lickle/state/signal'

const [count, setCount] = signal(0)
const doubled = memo(() => count() * 2)

const stop = effect(() => console.log('doubled:', doubled()))
setCount(1) // logs "doubled: 2"
stop()

For isolated dependency graphs, use createScope:

import { createScope } from '@lickle/state/signal'
const { signal, effect } = createScope()

License

MIT © Dan Beaven