@rvanbaalen/signals

A lightweight, decoupled pub/sub signal system for building reactive web applications with organized state management and clean component communication.

NPM Downloads GitHub License NPM Version

Description

Signals is a minimalist library that implements the signals pattern (also known as pub/sub or event emitter) for JavaScript applications. It helps you build decoupled, reactive components that can communicate without direct dependencies.

Key features:

  • Signal: Basic pub/sub implementation with connect/disconnect lifecycle management
  • SignalGroup: Organize related signals into logical groups
  • Store: Generic state container with integrated signal emission

Installation

Install the package via npm:

npm install @rvanbaalen/signals

Usage

Basic Signal Usage

import { Signal } from '@rvanbaalen/signals';

// Create a signal
const clicked = new Signal();

// Connect a listener (returns a cleanup function)
const cleanup = clicked.connect((x, y) => {
  console.log(`Clicked at ${x}, ${y}`);
});

// Emit the signal
clicked.emit(100, 200); // Logs: "Clicked at 100, 200"

// Cleanup when done
cleanup();

Using SignalGroup

import { SignalGroup } from '@rvanbaalen/signals';

// Create a group of related signals
const userSignals = new SignalGroup();

// Get signals from the group (creates them if they don't exist)
const loggedIn = userSignals.get('loggedIn');
const loggedOut = userSignals.get('loggedOut');

// Connect to signals
loggedIn.connect((user) => {
  console.log(`${user.name} logged in`);
});

// Emit signals
loggedIn.emit({ id: 1, name: 'Alice' });

Using Store

import { createStore } from '@rvanbaalen/signals';

// Create a store with initial state and signal groups
const store = createStore(
  { count: 0, user: null },
  ['counter', 'user']
);

// Connect to signals
store.connect('counter', 'changed', (state, oldState) => {
  console.log(`Count changed from ${oldState.count} to ${state.count}`);
});

// Update state (automatically emits signals)
store.update({ count: store.state.count + 1 }, 'counter', 'changed');

// Select parts of state
const count = store.select('count');
const user = store.select('user');

Advanced Examples

Component Communication

import { SignalGroup } from '@rvanbaalen/signals';

// Create shared signal group
const appSignals = new SignalGroup();

// Component A
class ComponentA {
  constructor() {
    this.cleanup = appSignals.get('dataRequested').connect((id) => {
      const data = this.fetchData(id);
      appSignals.get('dataReady').emit(data);
    });
  }
  
  fetchData(id) {
    return { id, name: 'Example Item' };
  }
  
  destroy() {
    this.cleanup();
  }
}

// Component B (completely decoupled from A)
class ComponentB {
  constructor() {
    this.cleanup = appSignals.get('dataReady').connect((data) => {
      this.updateUI(data);
    });
  }
  
  requestData(id) {
    appSignals.get('dataRequested').emit(id);
  }
  
  updateUI(data) {
    console.log('Updating UI with:', data);
  }
  
  destroy() {
    this.cleanup();
  }
}

Development

# Install dependencies
npm install

# Run development server
npm run dev

# Build for production
npm run build

# Run linter
npm run lint

Contributing

Contributions are welcome! If you have any suggestions, improvements, or bug fixes, please open an issue or submit a pull request.

License

Distributed under the MIT License. See the LICENSE file for more information.