Beacon 
Lightweight reactive state management for Node.js backends


A lightweight reactive state library for Node.js backends. Enables reactive state management with automatic dependency tracking and efficient updates for server-side applications.
Table of Contents
Features
- 📶 Reactive state - Create reactive values that automatically track dependencies
- 🧮 Computed values - Derive values from other states with automatic updates
- 🔍 Fine-grained reactivity - Dependencies are tracked precisely at the state level
- 🏎️ Efficient updates - Only recompute values when dependencies change
- 📦 Batched updates - Group multiple updates for performance
- 🎯 Targeted subscriptions - Select and subscribe to specific parts of state objects
- 🧹 Automatic cleanup - Effects and computations automatically clean up dependencies
- ♻️ Cycle handling - Safely manages cyclic dependencies without crashing
- 🚨 Infinite loop detection - Automatically detects and prevents infinite update loops
- 🛠️ TypeScript-first - Full TypeScript support with generics
- 🪶 Lightweight - Zero dependencies
- ✅ Node.js compatibility - Works with Node.js LTS v20+ and v22+
Quick Start
npm install @nerdalytics/beacon --save-exact
import { state, derive, effect } from '@nerdalytics/beacon';
const count = state(0);
const doubled = derive(() => count() * 2);
effect(() => {
console.log(`Count: ${count()}, Doubled: ${doubled()}`);
});
count.set(5);
Core Concepts
Beacon is built around three core primitives:
- States: Mutable, reactive values
- Derived States: Read-only computed values that update automatically
- Effects: Side effects that run automatically when dependencies change
The library handles all the dependency tracking and updates automatically, so you can focus on your business logic.
API Reference
Version Compatibility
The table below tracks when features were introduced and when function signatures were changed.
state | v1.0.0 | v1000.2.0 | Added equalityFn parameter |
derive | v1.0.0 | v1000.0.0 | Renamed derived → derive |
effect | v1.0.0 | - | - |
batch | v1.0.0 | - | - |
select | v1000.0.0 | - | - |
lens | v1000.1.0 | - | - |
readonlyState | v1000.0.0 | - | - |
protectedState | v1000.0.0 | v1000.2.0 | Added equalityFn parameter |
Core Primitives
state<T>(initialValue: T, equalityFn?: (a: T, b: T) => boolean): State<T>
Since v1.0.0
The foundation of Beacon's reactivity system. Create with state() and use like a function.
import { state } from '@nerdalytics/beacon';
const counter = state(0);
console.log(counter());
counter.set(5);
console.log(counter());
counter.update(n => n + 1);
console.log(counter());
const deepCounter = state({ value: 0 }, (a, b) => {
return a.value === b.value;
});
deepCounter.set({ value: 0 });
derive<T>(fn: () => T): ReadOnlyState<T>
Since v1.0.0
Calculate values based on other states. Updates automatically when dependencies change.
import { state, derive } from '@nerdalytics/beacon';
const firstName = state('John');
const lastName = state('Doe');
const fullName = derive(() => `${firstName()} ${lastName()}`);
console.log(fullName());
firstName.set('Jane');
console.log(fullName());
effect(fn: () => void): () => void
Since v1.0.0
Run side effects when reactive values change.
import { state, effect } from '@nerdalytics/beacon';
const user = state({ name: 'Alice', loggedIn: false });
const cleanup = effect(() => {
console.log(`User ${user().name} is ${user().loggedIn ? 'online' : 'offline'}`);
});
user.update(u => ({ ...u, loggedIn: true }));
cleanup();
batch<T>(fn: () => T): T
Since v1.0.0
Group multiple updates to trigger effects only once.
import { state, effect, batch } from "@nerdalytics/beacon";
const count = state(0);
effect(() => {
console.log(`Count is ${count()}`);
});
count.set(1);
count.set(2);
batch(() => {
count.set(10);
count.set(20);
count.set(30);
});
select<T, R>(source: ReadOnlyState<T>, selectorFn: (state: T) => R, equalityFn?: (a: R, b: R) => boolean): ReadOnlyState<R>
Since v1000.0.0
Subscribe to specific parts of a state object.
import { state, select, effect } from '@nerdalytics/beacon';
const user = state({
profile: { name: 'Alice' },
preferences: { theme: 'dark' }
});
const nameState = select(user, u => u.profile.name);
effect(() => {
console.log(`Name: ${nameState()}`);
});
user.update(u => ({
...u,
profile: { ...u.profile, name: 'Bob' }
}));
user.update(u => ({
...u,
preferences: { ...u.preferences, theme: 'light' }
}));
lens<T, K>(source: State<T>, accessor: (state: T) => K): State<K>
Since v1000.1.0
Two-way binding to deeply nested properties.
import { state, lens, effect } from "@nerdalytics/beacon";
const nested = state({
user: {
profile: {
settings: {
theme: "dark",
notifications: true
}
}
}
});
const themeLens = lens(nested, n => n.user.profile.settings.theme);
console.log(themeLens());
themeLens.set("light");
console.log(themeLens());
console.log(nested().user.profile.settings.theme);
Access Control
Control who can read vs. write to your state.
readonlyState<T>(state: State<T>): ReadOnlyState<T>
Since v1000.0.0
Creates a read-only view of a state, hiding mutation methods. Useful when you want to expose state to other parts of your application without allowing direct mutations.
import { state, readonlyState } from "@nerdalytics/beacon";
const counter = state(0);
const readonlyCounter = readonlyState(counter);
console.log(readonlyCounter());
counter.set(5);
console.log(readonlyCounter());
protectedState<T>(initialValue: T, equalityFn?: (a: T, b: T) => boolean): [ReadOnlyState<T>, WriteableState<T>]
Since v1000.0.0
Creates a state with separated read and write capabilities, returning a tuple of reader and writer. This pattern allows you to expose only the reading capability to consuming code while keeping the writing capability private.
import { protectedState } from "@nerdalytics/beacon";
const [getUser, setUser] = protectedState({ name: 'Alice' });
console.log(getUser());
setUser.set({ name: 'Bob' });
console.log(getUser());
function createProtectedCounter() {
const [getCount, setCount] = protectedState(0);
return {
value: getCount,
increment: () => setCount.update(n => n + 1),
decrement: () => setCount.update(n => n - 1)
};
}
const counter = createProtectedCounter();
console.log(counter.value());
counter.increment();
console.log(counter.value());
Advanced Features
Beacon includes several advanced capabilities that help you build robust applications.
Infinite Loop Protection
Beacon prevents common mistakes that could cause infinite loops:
import { state, effect } from '@nerdalytics/beacon';
const counter = state(0);
effect(() => {
const value = counter();
counter.set(value + 1);
});
const increment = () => counter.update(n => n + 1);
Automatic Cleanup
All subscriptions are automatically cleaned up when effects are unsubscribed:
import { state, effect } from '@nerdalytics/beacon';
const data = state({ loading: true, items: [] });
const cleanup = effect(() => {
if (data().loading) {
console.log('Loading...');
} else {
effect(() => {
console.log(`${data().items.length} items loaded`);
});
}
});
cleanup();
Custom Equality Functions
Control when subscribers are notified with custom equality checks. You can provide custom equality functions to state, select, and protectedState:
import { state, select, effect, protectedState } from '@nerdalytics/beacon';
const logMessages = state([], (a, b) => a === b);
logMessages.set(['System started']);
logMessages.set(['System started']);
const [getConfig, setConfig] = protectedState({ theme: 'dark' }, (a, b) => {
return a.theme === b.theme;
});
const list = state([1, 2, 3]);
const listLengthState = select(
list,
arr => arr.length,
(a, b) => a === b
);
effect(() => {
console.log(`List has ${listLengthState()} items`);
});
Design Philosophy
Beacon follows these key principles:
- Simplicity: Minimal API surface with powerful primitives
- Fine-grained reactivity: Track dependencies at exactly the right level
- Predictability: State changes flow predictably through the system
- Performance: Optimize for server workloads and memory efficiency
- Type safety: Full TypeScript support with generics
Architecture
Beacon is built around a centralized reactivity system with fine-grained dependency tracking. Here's how it works:
- Automatic Dependency Collection: When a state is read inside an effect, Beacon automatically records this dependency
- WeakMap-based Tracking: Uses WeakMaps for automatic garbage collection
- Topological Updates: Updates flow through the dependency graph in the correct order
- Memory-Efficient: Designed for long-running Node.js processes
Dependency Tracking
When a state is read inside an effect, Beacon automatically records this dependency relationship and sets up a subscription.
Infinite Loop Prevention
Beacon actively detects when an effect tries to update a state it depends on, preventing common infinite update cycles:
effect(() => {
const value = counter();
counter.set(value + 1);
});
Cyclic Dependencies
Beacon employs two complementary strategies for handling cyclical updates:
- Active Detection: The system tracks which states an effect reads from and writes to. If an effect attempts to directly update a state it depends on, Beacon throws a clear error.
- Safe Cycles: For indirect cycles and safe update patterns, Beacon uses a queue-based update system that won't crash even with cyclical dependencies. When states form a cycle where values eventually stabilize, the system handles these updates efficiently without stack overflows.
Development
# Install dependencies
npm install
# Run tests
npm test
| API Style | Functional approach (state(), derive()) | Class-based design (Signal.State, Signal.Computed) |
| Reading/Writing Pattern | Function call for reading (count()), methods for writing (count.set(5)) | Method-based access (get()/set()) |
| Framework Support | High-level abstractions like effect() and batch() | Lower-level primitives (Signal.subtle.Watcher) that frameworks build upon |
| Advanced Features | Focused on core reactivity | Includes introspection capabilities, watched/unwatched callbacks, and Signal.subtle namespace |
| Scope and Purpose | Practical Node.js use cases with minimal API surface | Standardization with robust interoperability between frameworks |
FAQ
Why "Beacon" Instead of "Signal"?
Beacon represents how the library broadcasts notifications when state changes—just like a lighthouse guides ships. The name avoids confusion with the TC39 proposal and similar libraries while accurately describing the core functionality.
How does Beacon handle memory management?
Beacon uses WeakMaps for dependency tracking, ensuring that unused states and effects can be garbage collected. When you unsubscribe an effect, all its internal subscriptions are automatically cleaned up.
Can I use Beacon with Express or other frameworks?
Yes! Beacon works well as a state management solution in any Node.js application:
import express from 'express';
import { state, effect } from '@nerdalytics/beacon';
const app = express();
const stats = state({ requests: 0, errors: 0 });
app.use((req, res, next) => {
stats.update(s => ({ ...s, requests: s.requests + 1 }));
next();
});
effect(() => {
console.log(`Stats: ${stats().requests} requests, ${stats().errors} errors`);
});
app.listen(3000);
Can Beacon be used in browser applications?
While Beacon is optimized for Node.js server-side applications, its core principles would work in browser environments. However, the library is specifically designed for backend use cases and hasn't been optimized for browser bundle sizes or DOM integration patterns.
License
This project is licensed under the MIT License. See the LICENSE file for details.