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

kida

v3.0.0

Published

A small state management library inspired by Nano Stores.

Readme

Kida

ESM-only package NPM version Dependencies status Install size Build status Coverage status

A small state management library inspired by Nano Stores and based on Agera.

  • Small. Around 2 kB for basic methods (minified and brotlied). Zero dependencies.
  • ~5x faster than Nano Stores.
  • Designed for best Tree-Shaking: only the code you use is included in your bundle.
  • TypeScript-first.
// store/users.ts
import { signal, push } from 'kida'

export const $users = signal<User[]>([])

export function addUser(user: User) {
  push($users, user)
}
// store/admins.ts
import { computed } from 'kida'
import { $users } from './users.js'

export const $admins = computed(() => $users().filter(user => user.isAdmin))
// components/admins.ts
import { record } from 'kida'
import { $admins } from '../stores/admins.js'

export function Admins() {
  return ul()(
    for_($admins, user => user.id)(
      $admin => li()(record($admin).$name)
    )
  )
}

Install

pnpm add kida
# or
npm i kida
# or
yarn add kida

Basics

Signal

Signal is a basic store type. It stores a single value.

import { signal } from 'kida'

const $count = signal(0)

$count($count() + 1)
// or
$count(count => count + 1)

To watch signal changes, use the effect function. Effect will be called immediately and every time the signal changes.

import { signal, effect } from 'kida'

const $count = signal(0)

const stop = effect(() => {
  console.log('Count:', $count())

  return () => {
    // Cleanup function. Will be called before effect update and before effect stop.
  }
})
// later you can stop effect
stop()

Computed

Computed is a signal that computes its value based on other signals.

import { computed } from 'kida'

const $firstName = signal('John')
const $lastName = signal('Doe')
const $fullName = computed(() => `${$firstName()} ${$lastName()}`)

console.log($fullName()) // John Doe

effectScope

effectScope creates a scope for effects. It allows to stop all effects in the scope at once.

import { signal, effectScope, effect } from 'kida'

const $a = signal(0)
const $b = signal(0)
const stop = effectScope(() => {
  effect(() => {
    console.log('A:', $a())
  })

  effectScope(() => {
    effect(() => {
      console.log('B:', $b())
    })
  })
})

stop() // stop all effects

onMountEffect

onMountEffect accepts a signal as a first argument to start effect on this signal mount.

import { signal, mountable, onMountEffect } from 'kida'

const $weather = mountable(signal('sunny'))
const $city = signal('Batumi')

onMountEffect($weather, () => {
  $weather(getWeather($city()))
})

onMountEffectScope

onMountEffectScope accepts a signal as a first argument to run effect scope on this signal mount.

import { signal, mountable, onMountEffectScope, effect } from 'kida'

const $weather = mountable(signal('sunny'))
const $city = signal('Batumi')

onMountEffectScope($weather, () => {
  effect(() => {
    console.log('Weather:', $weather())
  })

  effect(() => {
    console.log('City:', $city())
  })
})

Lifecycles

One of main feature of Kida is that you can create mountable signals. It allows to create lazy signals, which will use resources only if signal is really used in the UI.

  • Signal is mounted when one or more effects is attached to it.
  • Signal is unmounted when signal has no effects.

mountable method makes signal mountable.

onMount lifecycle method adds callback for mount and unmount events.

import { signal, mountable, onMount, effect } from 'kida'

const $count = mountable(signal(0))

onMount($count, () => {
  // Signal is now active
  return () => {
    // Signal is going to be inactive
  }
})

// will trigger mount event
const stop = effect(() => {
  console.log('Count:', $count())
})
// will trigger unmount event
stop()

For performance reasons, signal will move to disabled mode with 1 second delay after last effect unsubscribing. It allows to avoid unnecessary signal updates in case of fast mount/unmount events.

There are other lifecycle methods:

  • onStart($signal, () => Destroy): first effect was attached. Low-level method. It is better to use onMount for simple lazy signals.
  • onStop($signal, () => Destroy): last effect was detached. Low-level method. It is better to use onMount for simple lazy signals.

start

start method starts signal and returns function to stop it. It can be useful to write tests for signals.

import { signal, mountable, onMount, start } from 'kida'

const $count = mountable(signal(0))

onMount($count, () => {
  console.log('Signal started')
})

const stop = start($count) // Signal started

stop()

exec

exec method starts and immediately stops signal and returns signal value. It can be used to trigger onMount events.

import { signal, mountable, onMount, exec } from 'kida'

const $count = mountable(signal(0))

onMount($count, () => {
  console.log('Signal started')
})

exec($count) // Signal started and stopped

Complex data types

Record

record method gives access to properties of the object as child signals.

import { record } from 'kida'

const $user = record({ name: 'Dan', age: 30 })
const $name = $user.$name

console.log($name()) // Dan

Also record can be created from another signal.

const $userRecord = record($computedUser)

record method caches child signals in the parent signal. So you can call record multiple times on same signal without performance issues.

import { signal, record } from 'kida'

const $user = signal({ name: 'Dan', age: 30 })
const $name = record($user).$name
const $age = record($user).$age

Deep Record

deepRecord method gives access to nested properties of the object as child signals.

import { deepRecord } from 'kida'

const $user = deepRecord({ name: 'Dan', address: { city: 'Batumi' } })
const $city = $user.$address.$city

console.log($city()) // Batumi

Also deep record can be created from another signal.

const $userRecord = deepRecord($computedUser)

Array

atIndex method creates a signal for a specific index of an array.

import { signal, atIndex } from 'kida'

const $users = signal(['Dan', 'John', 'Alice'])
const $firstUser = atIndex($users, 0)

console.log($firstUser()) // Dan

$firstUser('Bob')

console.log($users()) // ['Bob', 'John', 'Alice']

atIndex supports dynamic indexes.

import { signal, atIndex } from 'kida'

const $users = signal(['Dan', 'John', 'Alice'])
const $index = signal(0)
const $user = atIndex($users, $index)

console.log($user()) // Dan

$index(1)

console.log($user()) // John

There are also other methods to work with arrays:

  • updateArray($array, fn) - update the value of the array signal using a function.
  • push($array, ...values) - add values to the array signal.
  • pop($array) - removes the last element from an array signal and returns it.
  • shift($array) - removes the first element from an array signal and returns it.
  • unshift($array, ...values) - inserts new elements at the start of an array signal, and returns the new length of the array.
  • setIndex($array, index, value) - set value at index in the array signal.
  • deleteIndex($array, index) - delete element at index from the array signal.

Object

atKey method creates a signal for a specific key of an object.

import { signal, atKey } from 'kida'

const $users = signal({
  2: 'Dan',
  4: 'John',
  6: 'Alice'
})
const $atId4 = atKey($users, 4)

console.log($atId4()) // John

$atId4('Bob')

console.log($users()) // { 2: 'Dan', 4: 'Bob', 6: 'Alice' }

atKey supports dynamic keys.

import { signal, atKey } from 'kida'

const $users = signal({
  2: 'Dan',
  4: 'John',
  6: 'Alice'
})
const $id = signal(4)
const $user = atKey($users, $id)

console.log($user()) // John

$id(6)

console.log($user()) // Alice

There are also other methods to work with object maps:

  • setKey($object, key, value) - set value by key to the object signal.
  • deleteKey($object, key) - delete item by key from the object signal.

child

child method creates a signal for a property of a parent signal with a custom writer: atIndex and atKey are built on it. The writer gets the parent value, the key and the new value, and returns the new parent value. The child is writable when the parent is writable, and read-only otherwise.

import { signal, child, assignKey } from 'kida'

const $user = signal({ name: 'Dan', age: 30 })
const $name = child($user, 'name', assignKey)

$name('Alice')

console.log($user()) // { name: 'Alice', age: 30 }

Dependency injection

The dependency injection system enables modular architecture and makes testing easier by allowing dependencies to be easily replaced with mocks. It also plays a critical role in SSR scenarios by isolating state between requests.

Use an injectable function or class with inject to retrieve dependencies:

import { inject, Injectable$, signal, mountable, onMountEffect, action } from 'kida'

function User$() {
  const $userId = signal(null)
  const $user = mountable(signal(null))

  const fetchUser = action(async (id) => {
    if (typeof id !== 'number') {
      $user(null)
      return
    }

    const response = await fetch(`/user/${id}`)
    const user = await response.json()

    $user(user)
  })

  onMountEffect($user, () => {
    fetchUser($userId())
  })

  return { $userId, $user }
}

class ApiClient$ extends Injectable$ {
  getUser(id: number) {
    return fetch(`/user/${id}`).then(response => response.json())
  }
}

Call inject(Factory$) inside another injectable to compose dependencies:

import { inject, signal } from 'kida'

function App$() {
  const { $user } = inject(User$)
  // ...
}

provide / InjectionContext / run

Use provide to override dependencies with custom values. Pass the providers to InjectionContext and run your code within it using run:

import { InjectionContext, provide, inject, run } from 'kida'

function Theme$() {
  return 'light'
}

const context = new InjectionContext([
  provide(Theme$, 'dark')
])

run(context, () => {
  const theme = inject(Theme$) // 'dark'
})

This pattern is especially useful in tests to mock dependencies:

import { InjectionContext, provide, inject, run } from 'kida'

const context = new InjectionContext([
  provide(ApiClient$, mockApiClient)
])

run(context, () => {
  const { $user } = inject(User$)
  // $user will use mockApiClient
})

Nested contexts

Pass a parent as the second argument of InjectionContext to create a child context. A child sees everything provided above it, and its own providers override the ones above.

A dependency that is not provided is created once and shared as widely as its inputs allow: it lives in the deepest context that provides something it reads, directly or through the dependencies it injects, and in the top context when it reads nothing provided. Every context below takes it from there:

import { InjectionContext, provide, inject } from 'kida'

function Theme$() {
  return 'light'
}

function Palette$() {
  return { theme: inject(Theme$) }
}

function Session$() {
  return { user: null }
}

const app = new InjectionContext()
const dark = new InjectionContext([provide(Theme$, 'dark')], app)

inject(Session$, dark) === inject(Session$, app) // true, Session$ reads nothing that dark provides
inject(Palette$, dark) === inject(Palette$, app) // false, dark has its own Palette$ for its Theme$

provideAs

provideAs resolves a token as another injectable. Unlike provide, which takes a ready value, it takes an injectable: the alias is created on first use, injects its own dependencies, and the token and the alias share one instance:

import { InjectionContext, provideAs, inject } from 'kida'

function Api$() {
  return { getUser: (id: number) => fetch(`/user/${id}`).then(response => response.json()) }
}

function MockApi$() {
  return { getUser: (id: number) => Promise.resolve({ id, name: 'Test User' }) }
}

const context = new InjectionContext([provideAs(Api$, MockApi$)])

inject(Api$, context) === inject(MockApi$, context) // true

When a token is provided twice in one array, the last provider wins, for values and aliases alike.

Utils

isSignal

isSignal method checks if the value is a signal.

import { isSignal, signal } from 'kida'

isSignal(signal(1)) // true

toSignal

toSignal method converts any value to signal or returns signal as is.

import { toSignal, computed } from 'kida'

const $count = toSignal(0) // WritableSignal<number>
const $double = toSignal(computed(() => $count() * 2)) // ReadableSignal<number>

length

length method creates a signal that tracks the length property of the object.

import { signal, length } from 'kida'

const $users = signal(['Dan', 'John', 'Alice'])
const $count = length($users)

boolean

boolean method creates a signal that converts the value to a boolean.

import { signal, boolean } from 'kida'

const $user = signal(null)
const $hasUser = boolean($user)

latest

latest method creates a signal with the value of the source that changed last. For example, a search field paired with a URL parameter: typing wins over the URL, navigation wins over what was typed.

import { signal, latest } from 'kida'

const $searchQuery = signal('') // search field
const $searchParam = signal('kida') // URL parameter
const $search = latest($searchQuery, $searchParam)

console.log($search()) // kida

$searchQuery('agera')

console.log($search()) // agera

$searchParam('nano')

console.log($search()) // nano

On the first read, and when several sources changed since the previous read (e.g. inside a batch), the last source in the argument list wins. Writing a value equal to the current one is not a change.

$get

$get method gets the value from the signal or returns the given value.

import { signal, $get } from 'kida'

$get(signal(1)) // 1
$get(1) // 1

get

get method gets the value from the signal or returns the given value without tracking (untracked).

import { signal, get } from 'kida'

get(signal(1)) // 1
get(1) // 1

composeDestroys

composeDestroys method composes multiple destroy functions into one.

import { composeDestroys, effect } from 'kida'

effect(() => composeDestroys(
  intervalLogger($interval),
  windowResizeLogger($size)
))

isEmpty

isEmpty checks if the value is null or undefined.

import { isEmpty } from 'kida'

isEmpty(null) // true
isEmpty(undefined) // true
isEmpty(0) // false
isEmpty('') // false

resolved

resolved accepts a promise or promise accessor and returns a [$result, $error, $pending] tuple of reactive signals. When the source changes, stale data is preserved while the new promise is pending, and the previous promise result is ignored.

import { signal, computed, resolved } from 'kida'

function getPosition(options?: PositionOptions) {
  return new Promise<GeolocationPosition>((resolve, reject) => {
    navigator.geolocation.getCurrentPosition(resolve, reject, options)
  })
}

const $highAccuracy = signal(false)
const [$position, $error, $pending] = resolved(
  computed(() => getPosition({ enableHighAccuracy: $highAccuracy() }))
)

A source value that is not a promise, a falsy one included, becomes the result as is. Express an empty state with the type, e.g. T | null.

Each promise from the source is attached as a task, so waitTasks of any of the three signals waits until it settles. Like any computed, a signal of the tuple links the source only on its first read: read it before waiting.

[!NOTE] For remote data fetching with caching, request deduplication, cancellation, refetching, and other advanced features, consider using @nano_kit/query instead.

Why?

Nano Stores is a great library with wonderful idea of stores with lifecycles. But it has some drawbacks:

  • Performance. Nano Stores is slow. Kida is ~5x faster than Nano Stores.
  • DX. Kida is focused more on DX than on bundle size. Nano Stores is smaller, but (to my mind) have worse API.
  • SSR. Nano Stores has no support for SSR. Kida has a built-in dependency injection system and serialization methods to work with SSR.

| BenchmarkThroughput avg (ops/s) | Kida / Agera | Alien Signals | Nano Stores | | ------- | --------- | ------- | ------ | | signal | 15 048 243 ± 0.01% | 18 967 631 ± 0.00% | 1 389 555 ± 0.02% | | computed | 1 208 888 ± 0.02% | 1 331 229 ± 0.02% | 187 524 ± 0.15% | | effect | 3 257 518 ± 0.01% | 3 463 918 ± 0.01% | 955 623 ± 0.03% |