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

@torq-js/reactivity

v0.0.1

Published

A fine-grained reactivity system built for JavaScript. Torq provides signal-based primitives that aim to feel native to the language while delivering precise, efficient reactive updates.

Readme

Torq Reactivity

A fine-grained reactivity system built for JavaScript. Torq provides signal-based primitives that aim to feel native to the language while delivering precise, efficient reactive updates.

Features

  • Fine-grained reactivity - Track dependencies at the property level for minimal re-computation
  • Built on Observables - Reactive primitives are observables compliant with the TC39 Observable proposal
  • Lazy by default - Computed values only evaluate when accessed or observed
  • Automatic cleanup - Scopes automatically dispose child effects and dependencies
  • Batched updates - Effects batch multiple synchronous changes into a single run
  • Lifecycle control - Dispose refs, effects, and scopes manually or via AbortSignal

Installation

npm install @torq-js/reactivity

Quick Start

Refs - Reactive Values

Refs are reactive containers for values that implement the Observable protocol. Read with .get(), write with .set(), and subscribe to changes with .subscribe().

import { Ref } from '@torq-js/reactivity';

const count = Ref(0);

console.log(count.get()); // 0

count.set(5);
console.log(count.get()); // 5

Assigning a source ref to another target ref, will keep the target up to date with the source until the target is assigned another value

const source = Ref(0);
const target = Ref(source);

source.set(1);
console.log(target.get()); // 1

target.set(999); // reassigning the target ref will break the connection
console.log(target.get()); // 999

source.set(2); 
console.log(target.get()); // still 999

Computed - Derived Values

Computed refs derive their values from other refs. They're lazy and cache automatically.

const count = Ref(1);
const doubled = Ref.computed(() => count.get() * 2);

console.log(doubled.get()); // 2

count.set(5);
console.log(doubled.get()); // 10

Computed refs support both getters and setters:

const firstName = Ref('Rick');
const lastName = Ref('Sanchez');

const fullName = Ref.computed({
  get: () => `${firstName.get()} ${lastName.get()}`,
  set: (value) => {
    const [first, last] = value.split(' ');
    firstName.set(first);
    lastName.set(last);
  }
});

console.log(fullName.get()); // "Rick Sanchez"

fullName.set('Morty Smith');
console.log(firstName.get()); // "Morty"
console.log(lastName.get()); // "Smith"

Effects - Reactive Side Effects

Effects automatically track their dependencies and re-run when those dependencies change. Updates are batched in a microtask.

import { Effect } from '@torq-js/reactivity';

const count = Ref(0);

Effect(() => {
  console.log('Count is:', count.get());
});
// Logs: "Count is: 0"

count.set(1);
count.set(2);
count.set(3);
// After microtask, logs once: "Count is: 3"

Effects can be controlled programmatically:

const effect = Effect(() => {
  console.log(count.get());
});

effect.disable(); // Stop reacting to changes
effect.enable();  // Resume reacting
effect.dispose(); // Permanently cleanup

Structs - Reactive Objects

Structs are objects where each property is backed by a stable ref. When you access a property, you get the unwrapped value. When you set a property, the underlying ref is updated and subscribers are notified.

import { Struct } from '@torq-js/reactivity';

const user = Struct({
  firstName: 'Rick',
  lastName: 'Sanchez'
});

// Property access returns unwrapped values
console.log(user.firstName); // "Rick"

// Property assignment updates the underlying ref
user.firstName = 'Morty';

// Computed refs track struct properties automatically
const greeting = Ref.computed(() => 
  `Hello, ${user.firstName} ${user.lastName}!`
);

console.log(greeting.get()); // "Hello, Morty Sanchez!"

// Access the underlying ref for a property
const firstNameRef = Struct.ref(user, 'firstName');
console.log(firstNameRef.get()); // "Morty"

Structs automatically unwrap refs when assigned to properties:

const count = Ref(0);
const data = Struct({ value: 0 });

// Assigning a ref unwraps it automatically
data.value = count;
console.log(data.value); // 0 (unwrapped)

// Just like when assigning to a ref, until it is assigne to again, 
// the struct property will stay in sync with the assigned ref
count.set(5);
console.log(data.value); // 5 (updates automatically)

Structs support getters and setters:

const person = Struct({
  firstName: 'Rick',
  lastName: 'Sanchez',
  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  },
  set fullName(value) {
    const [first, last] = value.split(' ');
    this.firstName = first;
    this.lastName = last;
  }
});

console.log(person.fullName); // "Rick Sanchez"
person.fullName = 'Morty Smith';
console.log(person.firstName); // "Morty"

Scopes - Lifecycle Management

Scopes manage the lifecycle of effects and their dependencies. When a scope disposes, all its children dispose too.

import { Scope } from '@torq-js/reactivity';

const scope = Scope(() => {
    Effect(() => {
        console.log('This effect is scoped!')
    })
});

Effect(() => {
  console.log('This effect is scoped too!');
}, { scope });

// Cleanup everything
scope.dispose();

Core Concepts

Refs are Signals

Torq's refs are inspired by the TC39 Signals proposal, sharing the same core API pattern of .get() and .set() methods for reactive values. If you're familiar with signals from other frameworks or the proposal itself, refs will feel immediately familiar:

const count = Ref(0);
console.log(count.get()); // 0

count.set(5);
console.log(count.get()); // 5

Refs are Observables

Torq also embraces the TC39 Observable proposal as a first-class feature. Every ref is an observable that you can directly subscribe to:

const count = Ref(0);

count.subscribe((value) => console.log('Value: ', value));

count.set(1); // Logs: "Value: 1"

This makes refs fully interoperable with any library or framework that understands observables. Think of refs as the union of signals and observables: you get automatic dependency tracking and explicit subscription control in one primitive.

Dependency Tracking

Torq uses automatic dependency tracking. When you call .get() inside a computed ref or effect, that ref is tracked as a dependency.

const condition = Ref(true);
const a = Ref(1);
const b = Ref(2);

const result = Ref.computed(() => {
  return condition.get() ? a.get() : b.get();
});

// Initially depends on 'condition' and 'a'
console.log(result.get()); // 1

condition.set(false);
// Now depends on 'condition' and 'b' (not 'a')
console.log(result.get()); // 2

Smart Caching

Computed refs cache their results and only recalculate when dependencies actually change, using equality checking to minimize recomputation:

const count = Ref(1);
const isOdd = Ref.computed(() => count.get() % 2 === 1);

isOdd.subscribe((value) => console.log('Is odd: ', value));

count.set(4); // This triggers the subscription in a microtask which will log "Is odd: false"

count.set(6); // This does not trigger the subscription because isOdd is still false

Lifecycle Control

Torq provides multiple approaches to manage the lifecycle of reactive primitives:

Manual disposal - Call dispose() directly on refs, effects, or scopes:

const effect = Effect(() => console.log('Running'));
effect.dispose();

AbortSignal - Use standard AbortSignal for automatic cleanup:

const controller = new AbortController();

const effect = Effect(() => {
  console.log(count.get());
}, { signal: controller.signal });

controller.abort(); // Disposes the effect

Scopes - Group related effects and dispose them all at once:

import { Scope } from '@torq-js/reactivity';

const scope = Scope();

// All effects created with this scope will be cleaned up together
Effect(() => console.log('Effect 1'), { scope });
Effect(() => console.log('Effect 2'), { scope });
Effect(() => console.log('Effect 3'), { scope });

// Disposes all three effects and their dependencies
scope.dispose();

Scopes automatically form parent-child hierarchies. When a scope is disposed, all of its child scopes and their effects are disposed as well, making it easy to manage complex reactive graphs.

Advanced Features

Batched Updates and Scheduling

Both effects and computed refs intelligently handle multiple dependency changes using microtask-based batching. Multiple synchronous updates are coalesced into a single recomputation:

const a = Ref(1);
const b = Ref(2);
const c = Ref(3);

// Effect batches multiple changes
Effect(() => {
  console.log('Sum:', a.get() + b.get() + c.get());
});
// Logs: "Sum: 6"

// Computed ref with subscriber also batches
const product = Ref.computed(() => a.get() * b.get() * c.get());
product.subscribe((value) => console.log('Product:', value));

// Multiple synchronous updates
a.set(10);
b.set(20);
c.set(30);

// After microtask:
// Logs "Sum: 60" once
// Logs "Product: 6000" once

Nested Scope Structure

Scopes form parent-child hierarchies automatically. When you create effects, computed refs, or scopes inside a reactive context, they become children of that context. Disposing a parent scope disposes the entire tree:

// Create a parent scope with a setup function
const parentScope = Scope(() => {
  // Create child scopes within the parent
  Scope(() => console.log('Child scope 1'));
  
  // Can also create effects as children
  Effect(() => console.log('Child effect'));

  // And computeds
  Ref.computed(() => someComputeFunction());
});

parentScope.dispose(); // All children are disposed

You can also explicitly specify a parent scope using the scope option:

const parentScope = Scope();

// Explicitly attach to parent scope
Effect(() => console.log('Effect 1'), { scope: parentScope });
Scope(() => console.log('Child scope'), { scope: parentScope });
Ref.computed(() => someValue.get(), { scope: parentScope });

// Disposing parent disposes all explicitly attached children
parentScope.dispose();

This hierarchical structure makes it easy to manage complex reactive graphs without manual cleanup.

Effects and Computeds are Scopes

Effects and computed refs are themselves scopes. This means they automatically participate in the scope hierarchy and can have child scopes of their own:

const count = Ref(0);

const doubled = Ref.computed(() => {
  console.log('Computing doubled');
  
  // Computed refs can have child scopes! But they shouldn't...😅
  Scope(() => {
    Effect(() => console.log('Side effect from computed'));
  });
  
  return count.get() * 2;
});

Detached Scopes

Scopes can be created without a parent by explicitly passing null as the scope option. This creates a detached scope that won't be automatically disposed when any parent context ends:

Effect(() => {
  // This effect is detached - it won't be disposed when the parent effect re-runs or is disposed
  const detachedEffect = Effect(() => {
    console.log('Detached scope setup');
  }, { scope: null });
});

Detached scopes are useful when you need reactive computations to outlive their creation context, but remember you're responsible for disposing them manually.

Observing Dependencies

Scopes track all observables that are accessed within them. You can inspect these dependencies using the observables() iterator:

const count = Ref(0);
const name = Ref('Alice');

const scope = Scope(() => {
  count.get();
  name.get();
});

// Inspect what observables this scope depends on
for (const observable of scope.observables()) {
  console.log('Scope observes:', observable);
}

const dependencies = Array.from(scope.observables());
console.log(`Scope has ${dependencies.length} dependencies`);

This is particularly useful for debugging, introspection, or building developer tools that need to understand the reactive graph.

Inspecting Child Scopes

Scopes also expose their child scopes through the scopes() iterator, allowing you to traverse the entire scope hierarchy:

const parentScope = Scope(() => {
  Scope(() => console.log('Child 1'));
  Effect(() => console.log('Child effect'));
  Ref.computed(() => someComputeFunction());
});

// Iterate over direct children
for (const child of parentScope.scopes()) {
  console.log('Child scope:', child);
}

// Convert to array
const children = Array.from(parentScope.scopes());
console.log(`Parent has ${children.length} child scopes`);

// Recursively traverse the entire tree
function traverseScopes(scope, depth = 0) {
  const indent = '  '.repeat(depth);
  console.log(`${indent}Scope`);
  
  for (const child of scope.scopes()) {
    traverseScopes(child, depth + 1);
  }
}

traverseScopes(parentScope);

This enables powerful introspection capabilities for debugging complex reactive applications or building development tools that visualize the reactive graph structure.

Design Philosophy and Goals

Torq is built on the principle that the chief concern of reactivity systems is providing a means of subscribing to changes. This is why refs are observables first and foremost and why subscriptions aren't hidden or secondary, they're central to the design.

From this philosophy, Torq was built with these goals in mind:

  • Feel native to JavaScript - Match ecosystem conventions and work with standard protocols
  • Composable primitives - Small, focused building blocks that combine naturally
  • Scale to complexity - Handle large dependency graphs efficiently

License

MIT