W WRNexusJS
Frontend

@wrnexus/reactive

Small type-safe reactive signal primitives.

bun add @wrnexus/reactive@0.2.15
Tiny, type-safe reactive primitives (signals) with zero dependencies.

Part of the WRNexusJS framework — an SSR-first, Bun-native full-stack web framework.

Overview

@wrnexus/reactive is the seed of WRNexusJS's reactivity layer: a minimal signal primitive that holds a value, notifies subscribers when it changes, and hands back an unsubscribe function. It is deliberately small and framework-agnostic — it powers nothing on its own, but is shaped so client islands (and later the .wrn compiler's state blocks) can build reactive bindings on top of it. Reach for it when you need observable state without pulling in a full reactivity library.

Installation

bun add @wrnexus/reactive
Private package — the machine must be authenticated to the wrnexus npm org
(a read token in ~/.npmrc). Requires Bun (Node is not supported).

API

The package has a single entry point (.) exporting one function and three types.

signal<T>(initial: T): Signal<T>

Creates a reactive signal seeded with initial. Returns a Signal<T>:

MemberSignatureDescription
get(): TRead the current value.
set(next: T): voidWrite a new value. Subscribers run only when the value actually changes (compared with Object.is).
update(fn: (current: T) => T): voidApply a function to the current value; equivalent to set(fn(get())).
subscribe(fn: Subscriber<T>): UnsubscribeRegister a subscriber; returns a function that removes it.

Types

type Subscriber<T> = (value: T) => void;
type Unsubscribe = () => void;

interface Signal<T> {
  get(): T;
  set(next: T): void;
  update(fn: (current: T) => T): void;
  subscribe(fn: Subscriber<T>): Unsubscribe;
}

Notes on semantics:

  • No-op updates are skipped. set compares the incoming value to the current
  • one with Object.is; identical values do not notify subscribers.

  • Safe unsubscribe during notification. Subscribers are iterated over a copy of
  • the subscriber set, so a subscriber may call its own (or another's) unsubscribe while a notification is in flight.

Usage

import { signal } from "@wrnexus/reactive";

const count = signal(0);

count.get(); // 0

// Subscribe; the returned function unsubscribes.
const off = count.subscribe((value) => {
  console.log("count is now", value);
});

count.set(1); // logs: count is now 1
count.set(1); // no-op — value unchanged, no notification
count.update((n) => n + 1); // logs: count is now 2

off(); // stop listening
count.set(3); // nothing logged

Typed signals infer T from the initial value, or can be annotated explicitly:

import { signal, type Signal } from "@wrnexus/reactive";

const user: Signal<{ name: string } | null> = signal(null);
user.set({ name: "Ada" });

Requirements / Notes

  • Bun-only. Distributed as TypeScript source (main/exports point at
  • src/index.ts); consume it under Bun, which runs .ts directly.

  • Zero dependencies. The only runtime API used is the standard Object.is.
  • Foundational primitive for WRNexusJS client islands and the forthcoming .wrn
  • compiler state blocks.

Complete TypeScript API

This declaration is generated from the exact published package and lists its exported functions, classes, interfaces, and types.

/**
 * A minimal, type-safe reactive signal with zero dependencies.
 *
 * This is the seed of the framework's reactivity. Today it powers nothing on
 * its own, but it is shaped so client islands (and later the `.wrn` compiler's
 * `state` blocks) can build reactive bindings on top of it.
 *
 *   const count = signal(0)
 *   count.get()                 // 0
 *   count.set(1)                // notifies subscribers
 *   const off = count.subscribe(v => console.log(v))
 *   off()                       // unsubscribe
 */
type Subscriber<T> = (value: T) => void;
type Unsubscribe = () => void;
interface Signal<T> {
    /** Read the current value. */
    get(): T;
    /** Write a new value; subscribers run only when the value actually changes. */
    set(next: T): void;
    /** Apply a function to the current value. */
    update(fn: (current: T) => T): void;
    /** Subscribe to changes; returns an unsubscribe function. */
    subscribe(fn: Subscriber<T>): Unsubscribe;
}
declare function signal<T>(initial: T): Signal<T>;

export { type Signal, type Subscriber, type Unsubscribe, signal };

Examples

Copy-ready examples taken from this package's published documentation.

Example 1

bun add @wrnexus/reactive

Example 2

type Subscriber<T> = (value: T) => void;
type Unsubscribe = () => void;

interface Signal<T> {
  get(): T;
  set(next: T): void;
  update(fn: (current: T) => T): void;
  subscribe(fn: Subscriber<T>): Unsubscribe;
}

Example 3

import { signal } from "@wrnexus/reactive";

const count = signal(0);

count.get(); // 0

// Subscribe; the returned function unsubscribes.
const off = count.subscribe((value) => {
  console.log("count is now", value);
});

count.set(1); // logs: count is now 1
count.set(1); // no-op — value unchanged, no notification
count.update((n) => n + 1); // logs: count is now 2

off(); // stop listening
count.set(3); // nothing logged

Example 4

import { signal, type Signal } from "@wrnexus/reactive";

const user: Signal<{ name: string } | null> = signal(null);
user.set({ name: "Ada" });