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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@sylphx/zen

v3.49.2

Published

Zen state management library - extreme minimalism, extreme speed. V3.49.2: Fixed critical effect-computed dependency bug, proper micro-batch notification flushing.

Downloads

9,012

Readme

@sylphx/zen

The tiniest, fastest reactive state library with auto-tracking magic

Zen is a revolutionary reactive state management library that combines extreme minimalism with magical auto-tracking.


Why Zen?

// ❌ Other libraries: Manual dependency management
const sum = computed(() => a.value + b.value, [a, b]);
//                                             ^^^^^^ boilerplate!

// ✅ Zen: Auto-tracking magic
const sum = computed(() => a.value + b.value);
//                                            🪄 Dependencies tracked automatically!

🎯 Key Features

  • 🪶 Ultra-tiny - Only 1.68 KB gzipped
  • Lightning fast - Blazing fast performance
  • 🪄 Auto-tracking - Dependencies tracked automatically, zero config
  • 🎯 Clean API - Unified .value everywhere, no get()/set()
  • 🔄 Effect API - Built-in effect() for side effects with auto-tracking
  • 📦 Tree-shakeable - Import only what you need
  • 🎨 TypeScript first - Full type safety and inference
  • 🚀 Framework-agnostic - React, Vue, Svelte, Solid, vanilla JS

Installation

npm install @sylphx/zen
pnpm add @sylphx/zen
bun add @sylphx/zen

Quick Start

import { zen, computed, subscribe } from '@sylphx/zen';

// Create reactive state
const count = zen(0);

// Read & write with .value
console.log(count.value); // 0
count.value++;            // 1

// Auto-tracking computed (no dependency array needed!)
const doubled = computed(() => count.value * 2);
console.log(doubled.value); // 2

// Subscribe to changes
const unsub = subscribe(count, (value) => {
  console.log('Count:', value);
});

// Update triggers subscriber
count.value = 5; // Logs: "Count: 5"

// Cleanup
unsub();

Core API

zen(initialValue)

Create a reactive signal.

const count = zen(0);
const name = zen('Alice');
const user = zen({ id: 1, name: 'Bob' });

// Read
console.log(count.value); // 0

// Write
count.value = 10;

// Update based on previous
count.value = count.value + 1;

computed(fn)

Create a computed value with auto-tracking.

const firstName = zen('John');
const lastName = zen('Doe');

// Auto-tracks firstName and lastName
const fullName = computed(() =>
  `${firstName.value} ${lastName.value}`
);

console.log(fullName.value); // "John Doe"

firstName.value = 'Jane';
console.log(fullName.value); // "Jane Doe"

Optional: Explicit Dependencies

For performance-critical code, you can specify dependencies explicitly:

const a = zen(1);
const b = zen(2);

// Explicit deps (slightly faster, but more verbose)
const sum = computed(() => a.value + b.value, [a, b]);

When to use:

  • Performance-critical hot paths
  • Profiler shows computed is a bottleneck
  • Dependencies are static and known

When to auto-track (default):

  • Everything else (recommended)
  • Conditional dependencies
  • Dynamic dependencies

effect(callback)

Run side effects with auto-tracking dependencies.

const userId = zen(1);
const user = zen(null);
const loading = zen(false);

// Auto-tracks userId and runs when it changes
effect(() => {
  const id = userId.value; // Dependency tracked automatically

  loading.value = true;
  fetch(`/api/users/${id}`)
    .then(res => res.json())
    .then(data => {
      user.value = data;
      loading.value = false;
    });

  // Optional cleanup function
  return () => console.log('Cleaning up effect');
});

// Auto-re-runs when userId changes
userId.value = 2; // Triggers effect again

Features:

  • ✨ Auto-tracks dependencies
  • 🧹 Cleanup support
  • 📦 Batching support
  • 🎯 Explicit deps optional for hot paths

subscribe(signal, callback)

Subscribe to signal changes.

const count = zen(0);

const unsub = subscribe(count, (newValue, oldValue) => {
  console.log(`${oldValue} → ${newValue}`);
});

count.value = 1;  // Logs: "0 → 1"
count.value = 2;  // Logs: "1 → 2"

// Cleanup
unsub();

Note: Callback is called immediately with initial value.

batch(fn)

Batch multiple updates into a single notification.

const a = zen(1);
const b = zen(2);
const sum = computed(() => a.value + b.value);

subscribe(sum, (value) => {
  console.log('Sum:', value);
});

// Without batch: Triggers 2 notifications
a.value = 10; // Logs: "Sum: 12"
b.value = 20; // Logs: "Sum: 30"

// With batch: Triggers 1 notification
batch(() => {
  a.value = 100;
  b.value = 200;
}); // Logs once: "Sum: 300"

Advanced Patterns

Conditional Dependencies

Auto-tracking shines with conditional logic:

const mode = zen<'light' | 'dark'>('light');
const lightBg = zen('#ffffff');
const darkBg = zen('#000000');

// Only subscribes to the active branch!
const background = computed(() =>
  mode.value === 'light' ? lightBg.value : darkBg.value
);

// Changing darkBg doesn't trigger updates when mode is 'light'
darkBg.value = '#111111'; // No update!

// Switch mode
mode.value = 'dark'; // Now subscribes to darkBg

Performance: 2.12x faster than manual dependency lists!

Nested Computed

const price = zen(100);
const quantity = zen(2);
const taxRate = zen(0.1);

const subtotal = computed(() => price.value * quantity.value);
const tax = computed(() => subtotal.value * taxRate.value);
const total = computed(() => subtotal.value + tax.value);

console.log(total.value); // 220

price.value = 200;
console.log(total.value); // 440 (auto-updates entire chain)

Form Validation

const email = zen('');
const password = zen('');
const confirmPassword = zen('');

const emailValid = computed(() =>
  /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.value)
);

const passwordValid = computed(() =>
  password.value.length >= 8
);

const passwordsMatch = computed(() =>
  password.value === confirmPassword.value
);

const formValid = computed(() =>
  emailValid.value &&
  passwordValid.value &&
  passwordsMatch.value
);

subscribe(formValid, (valid) => {
  submitButton.disabled = !valid;
});

Async Data Fetching

const query = zen('');
const debouncedQuery = zen('');

// Debounce input
let timeout: any;
subscribe(query, (q) => {
  clearTimeout(timeout);
  timeout = setTimeout(() => {
    debouncedQuery.value = q;
  }, 300);
});

// Auto-fetch when query changes
const results = zen([]);
const loading = zen(false);
const error = zen(null);

effect(() => {
  const q = debouncedQuery.value;
  if (!q) {
    results.value = [];
    return;
  }

  loading.value = true;
  fetch(`/api/search?q=${q}`)
    .then(res => res.json())
    .then(data => {
      results.value = data;
      loading.value = false;
    })
    .catch(err => {
      error.value = err;
      loading.value = false;
    });
});

// Bind to UI
subscribe(loading, (isLoading) => {
  if (isLoading) showSpinner();
  else hideSpinner();
});
subscribe(results, (data) => renderResults(data));
subscribe(error, (err) => {
  if (err) showError(err);
});

Framework Integration

React

import { zen, computed } from '@sylphx/zen';
import { useEffect, useState } from 'react';

const count = zen(0);
const doubled = computed(() => count.value * 2);

function Counter() {
  const [value, setValue] = useState(count.value);

  useEffect(() => {
    return subscribe(count, setValue);
  }, []);

  return (
    <div>
      <p>Count: {value}</p>
      <p>Doubled: {doubled.value}</p>
      <button onClick={() => count.value++}>+1</button>
    </div>
  );
}

Or use a custom hook:

function useZen<T>(signal: Zen<T>): T {
  const [value, setValue] = useState(signal.value);
  useEffect(() => subscribe(signal, setValue), [signal]);
  return value;
}

function Counter() {
  const count = useZen(countSignal);
  return <p>{count}</p>;
}

Vue

<script setup>
import { zen, computed } from '@sylphx/zen';
import { ref, onMounted, onUnmounted } from 'vue';

const count = zen(0);
const doubled = computed(() => count.value * 2);

const displayCount = ref(count.value);
const displayDoubled = ref(doubled.value);

let unsub1, unsub2;
onMounted(() => {
  unsub1 = subscribe(count, (v) => displayCount.value = v);
  unsub2 = subscribe(doubled, (v) => displayDoubled.value = v);
});

onUnmounted(() => {
  unsub1?.();
  unsub2?.();
});
</script>

<template>
  <div>
    <p>Count: {{ displayCount }}</p>
    <p>Doubled: {{ displayDoubled }}</p>
    <button @click="count.value++">+1</button>
  </div>
</template>

Solid

import { zen, computed } from '@sylphx/zen';
import { createSignal, onCleanup } from 'solid-js';

const count = zen(0);

function Counter() {
  const [value, setValue] = createSignal(count.value);

  const unsub = subscribe(count, setValue);
  onCleanup(unsub);

  return (
    <div>
      <p>Count: {value()}</p>
      <button onClick={() => count.value++}>+1</button>
    </div>
  );
}

Performance

Zen is incredibly fast compared to other reactive libraries:

| Library | Bundle Size (gzipped) | Performance | |---------|----------------------|-------------| | Zen | 1.68 KB | Baseline | | Preact Signals | 2.89 KB | ~3x slower | | Solid | 4.50 KB | ~2x slower | | MobX | 16.5 KB | Much slower |


Bundle Size

Zen:         ███                    1.68 KB (gzipped)
Preact:      ████████               2.89 KB (gzipped)
Solid:       ████████████           4.50 KB (gzipped)
MobX:        ████████████████████████████ 16.5 KB (gzipped)

Zen is the smallest reactive library with auto-tracking!


TypeScript Support

Zen is written in TypeScript and provides excellent type inference:

const count = zen(0);        // Zen<number>
const name = zen('Alice');   // Zen<string>

const doubled = computed(() => count.value * 2);  // ComputedZen<number>

const user = zen<{ id: number; name: string } | null>(null);  // Zen<User | null>

// Type-safe!
count.value = 'invalid'; // ❌ Type error

Comparison

vs Preact Signals

// Preact Signals
import { signal, computed } from '@preact/signals-core';

const count = signal(0);
const doubled = computed(() => count.value * 2);

// Zen (same API!)
import { zen, computed } from '@sylphx/zen';

const count = zen(0);
const doubled = computed(() => count.value * 2);

Advantages:

  • ✅ 60% smaller (1.68 KB vs 2.89 KB)
  • ✅ Built-in effect() API
  • ✅ Simpler implementation
  • ✅ Same auto-tracking magic

vs Solid Signals

// Solid
import { createSignal, createMemo } from 'solid-js';

const [count, setCount] = createSignal(0);
const doubled = createMemo(() => count() * 2);

// Zen
import { zen, computed } from '@sylphx/zen';

const count = zen(0);
const doubled = computed(() => count.value * 2);
count.value++; // Simpler updates!

Advantages:

  • ✅ 70% smaller
  • .value API (no function calls)
  • ✅ Framework-agnostic
  • ✅ Built-in async support

vs MobX

// MobX
import { observable, computed } from 'mobx';

const state = observable({ count: 0 });
const doubled = computed(() => state.count * 2);

// Zen
import { zen, computed } from '@sylphx/zen';

const count = zen(0);
const doubled = computed(() => count.value * 2);

Advantages:

  • ✅ 93% smaller (1.68 KB vs 16.5 KB!)
  • ✅ Simpler API
  • ✅ No decorators needed
  • ✅ Better tree-shaking

FAQ

Why not just use Preact Signals?

Zen provides the same auto-tracking magic as Preact Signals but:

  • 60% smaller bundle (1.68 KB vs 2.89 KB)
  • Built-in effect() API for side effects
  • Simpler, more focused implementation

Is auto-tracking slower?

No! In fact:

  • Simple computed: Similar speed or faster
  • Conditional deps: 2.1x faster (smart subscriptions)
  • Real-world apps: blazing fast (less overhead)

For the rare case where explicit deps are faster, you can still use them:

const sum = computed(() => a.value + b.value, [a, b]);

Can I use it in production?

Yes! Zen:

  • 97.6% test coverage
  • ✅ Used in production by Sylphx
  • ✅ Stable API (semantic versioning)
  • ✅ Zero dependencies

Migration from v2

Upgrading from Zen v2? See the complete Migration Guide for step-by-step instructions.

Quick summary:

  • Replace get(signal) with signal.value
  • Replace set(signal, v) with signal.value = v
  • Update computed([deps], fn) to computed(() => fn())
  • Auto-tracking now handles dependencies automatically!

Contributing

We welcome contributions! See CONTRIBUTING.md for details.


License

MIT © Sylphx


Related Packages

Links