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

@usefy/use-signal

v0.2.4

Published

A React hook for event-driven communication between components

Readme


Overview

@usefy/use-signal enables event-driven communication between React components without prop drilling or complex state management setup. Components subscribe to a shared "signal" by name, and when any component emits that signal, all subscribers receive an update.

Part of the @usefy ecosystem — a collection of production-ready React hooks designed for modern applications.

Why use-signal?

  • Zero Dependencies — Pure React implementation with no external dependencies
  • TypeScript First — Full type safety with exported interfaces
  • Stable Referencesemit and info maintain stable references across re-renders
  • SSR Compatible — Works seamlessly with Next.js, Remix, and other SSR frameworks
  • Lightweight — Minimal bundle footprint (~1KB minified + gzipped)
  • Well Tested — Comprehensive test coverage with Vitest
  • React 18+ Optimized — Uses useSyncExternalStore for concurrent mode compatibility

Use Cases

  • Dashboard Refresh — Refresh multiple widgets with a single button click
  • Form Reset — Reset multiple form sections simultaneously
  • Cache Invalidation — Invalidate and reload data across components
  • Multi-step Flows — Coordinate state across wizard steps
  • Event Broadcasting — Notify multiple listeners about system events

⚠️ What This Hook Is NOT

useSignal is NOT a global state management solution.

This hook is designed for lightweight event-driven communication—sharing simple "signals" between components without the overhead of complex state management setup.

If you need:

  • Complex shared state with derived values
  • Persistent state across page navigation
  • State that drives business logic
  • Fine-grained state updates with selectors

→ Use dedicated state management tools like React Context, Zustand, Jotai, Recoil, or Redux.

About info.data: The data payload feature exists for cases where you need to pass contextual information along with a signal (e.g., which item was clicked, what action was performed). It's meant for event metadata, not as a global state container.

// ✅ Good: Signal with contextual data
emit({ itemId: "123", action: "refresh" });

// ❌ Bad: Using as global state
emit({ user: userData, cart: cartItems, settings: appSettings });

Installation

# npm
npm install @usefy/use-signal

# yarn
yarn add @usefy/use-signal

# pnpm
pnpm add @usefy/use-signal

Peer Dependencies

This package requires React 18 or 19:

{
  "peerDependencies": {
    "react": "^18.0.0 || ^19.0.0"
  }
}

Quick Start

import { useSignal } from "@usefy/use-signal";
import { useEffect } from "react";

// Emitter Component
function RefreshButton() {
  const { emit } = useSignal("dashboard-refresh");

  return <button onClick={emit}>Refresh Dashboard</button>;
}

// Subscriber Component
function DataWidget() {
  const { signal } = useSignal("dashboard-refresh");

  useEffect(() => {
    fetchData(); // Refetch when signal changes
  }, [signal]);

  return <div>Widget Content</div>;
}

API Reference

useSignal(name, options?)

A hook that subscribes to a named signal and provides emit functionality.

Parameters

| Parameter | Type | Description | | --------- | --------------- | ---------------------------------- | | name | string | Unique identifier for the signal | | options | SignalOptions | Optional configuration (see below) |

Options

interface SignalOptions {
  emitOnMount?: boolean; // Emit when component mounts (default: false)
  onEmit?: () => void; // Callback executed on emit
  enabled?: boolean; // Enable/disable subscription (default: true)
  debounce?: number; // Debounce emit calls in milliseconds
}

Returns UseSignalReturn<T>

| Property | Type | Description | | -------- | ----------------- | ------------------------------------------- | | signal | number | Current version number (use in deps arrays) | | emit | (data?: T) => void | Function to emit the signal with optional data | | info | SignalInfo<T> | Metadata object (see below) |

SignalInfo

interface SignalInfo<T = unknown> {
  name: string; // Signal name
  subscriberCount: number; // Active subscribers
  timestamp: number; // Last emit timestamp
  emitCount: number; // Total emit count
  data: T | undefined; // Data passed with last emit
}

Note: info is a stable reference (ref-based) that doesn't trigger re-renders. Use signal in dependency arrays to react to changes, and access the latest info.data inside useEffect.


Examples

Dashboard Refresh Pattern

import { useSignal } from "@usefy/use-signal";
import { useEffect, useState } from "react";

function RefreshButton() {
  const { emit, info } = useSignal("dashboard-refresh");

  return (
    <button onClick={emit}>
      Refresh All ({info.subscriberCount} widgets)
    </button>
  );
}

function SalesChart() {
  const { signal } = useSignal("dashboard-refresh");
  const [data, setData] = useState([]);

  useEffect(() => {
    fetchSalesData().then(setData);
  }, [signal]);

  return <Chart data={data} />;
}

function UserStats() {
  const { signal } = useSignal("dashboard-refresh");
  const [stats, setStats] = useState(null);

  useEffect(() => {
    fetchUserStats().then(setStats);
  }, [signal]);

  return <Stats data={stats} />;
}

Form Reset

import { useSignal } from "@usefy/use-signal";

function ResetButton() {
  const { emit } = useSignal("form-reset");
  return <button onClick={emit}>Reset All Fields</button>;
}

function PersonalInfoSection() {
  const { signal } = useSignal("form-reset");
  const [name, setName] = useState("");
  const [email, setEmail] = useState("");

  useEffect(() => {
    if (signal > 0) {
      setName("");
      setEmail("");
    }
  }, [signal]);

  return (
    <section>
      <input value={name} onChange={(e) => setName(e.target.value)} />
      <input value={email} onChange={(e) => setEmail(e.target.value)} />
    </section>
  );
}

With Data Payload

import { useSignal } from "@usefy/use-signal";
import { useEffect } from "react";

interface NotificationData {
  type: "success" | "error" | "info";
  message: string;
}

function NotificationEmitter() {
  const { emit } = useSignal<NotificationData>("notification");

  return (
    <button
      onClick={() =>
        emit({ type: "success", message: "Operation completed!" })
      }
    >
      Send Notification
    </button>
  );
}

function NotificationReceiver() {
  const { signal, info } = useSignal<NotificationData>("notification");

  useEffect(() => {
    if (signal > 0 && info.data) {
      // info.data is guaranteed to contain the latest data
      console.log(`[${info.data.type}] ${info.data.message}`);
    }
  }, [signal]);

  return <div>Last: {info.data?.message ?? "No notifications"}</div>;
}

With Debounce

import { useSignal } from "@usefy/use-signal";

function SearchInput() {
  const { emit } = useSignal<string>("search-update", { debounce: 300 });

  return (
    <input
      type="text"
      onChange={(e) => {
        emit(e.target.value); // Debounced - uses latest value after 300ms
      }}
    />
  );
}

Conditional Subscription

import { useSignal } from "@usefy/use-signal";

function ConditionalWidget({ visible }: { visible: boolean }) {
  // Only subscribe when visible
  const { signal } = useSignal("updates", { enabled: visible });

  useEffect(() => {
    if (signal > 0) {
      refreshData();
    }
  }, [signal]);

  if (!visible) return null;
  return <div>Widget Content</div>;
}

With onEmit Callback

import { useSignal } from "@usefy/use-signal";

function LoggingEmitter() {
  const { emit } = useSignal("analytics-event", {
    onEmit: () => {
      console.log("Event emitted at", new Date().toISOString());
      trackAnalytics("signal_emitted");
    },
  });

  return <button onClick={emit}>Track Event</button>;
}

Emit on Mount

import { useSignal } from "@usefy/use-signal";

function AutoRefreshComponent() {
  const { signal } = useSignal("data-refresh", { emitOnMount: true });

  useEffect(() => {
    // This runs immediately on mount due to emitOnMount
    fetchLatestData();
  }, [signal]);

  return <div>Auto-refreshed content</div>;
}

Using Info for Conditional Logic

import { useSignal } from "@usefy/use-signal";

function SmartEmitter() {
  const { emit, info } = useSignal("notification");

  const handleClick = () => {
    // Only emit if there are subscribers
    if (info.subscriberCount > 0) {
      emit();
    } else {
      console.log("No subscribers, skipping emit");
    }
  };

  return (
    <button onClick={handleClick}>
      Notify ({info.subscriberCount} listeners)
    </button>
  );
}

TypeScript

This hook is written in TypeScript and exports all interfaces. Use generics to type the data payload.

import {
  useSignal,
  type UseSignalReturn,
  type SignalOptions,
  type SignalInfo,
} from "@usefy/use-signal";

// With typed data payload
interface MyEventData {
  action: string;
  payload: Record<string, unknown>;
}

const { signal, emit, info }: UseSignalReturn<MyEventData> = useSignal<MyEventData>(
  "my-signal",
  {
    emitOnMount: true,
    onEmit: () => console.log("emitted"),
    enabled: true,
    debounce: 100,
  }
);

// signal: number
// emit: (data?: MyEventData) => void
// info: SignalInfo<MyEventData>
// info.data: MyEventData | undefined

Performance

Stable References

The emit function and info object maintain stable references across re-renders:

const { emit, info } = useSignal<MyData>("my-signal");

// emit reference remains stable
useEffect(() => {
  // Safe to use as dependencies
}, [emit]);

// info is a ref-based object - stable reference, live values via getters
console.log(info.subscriberCount); // Always current
console.log(info.data); // Latest data from last emit

Data Ordering Guarantee

When emit(data) is called, the data is stored before the signal version increments:

const { signal, info } = useSignal<string>("my-signal");

useEffect(() => {
  // This is guaranteed: info.data contains the data passed to emit()
  // that triggered this signal change
  console.log(info.data); // Always the latest data
}, [signal]);

Minimal Re-renders

  • Only subscribed components re-render when signal is emitted
  • The signal value is a primitive number, optimized for dependency arrays
  • Uses React 18's useSyncExternalStore for optimal concurrent mode support

How It Works

  1. Global Store: A singleton Map stores signal data (version, subscribers, metadata, payload data)
  2. Subscription: Components subscribe via useSyncExternalStore
  3. Emit: Sets data → Increments version → Updates timestamp → Notifies all subscribers
  4. Cleanup: Automatic unsubscription on unmount

Key Design: Data is set before version increment to ensure useEffect callbacks always see the latest info.data.

┌─────────────┐     emit()      ┌─────────────┐
│  Component  │ ───────────────▶│   Signal    │
│   Emitter   │                 │   Store     │
└─────────────┘                 └──────┬──────┘
                                       │
                        notify all subscribers
                                       │
              ┌────────────────────────┼────────────────────────┐
              ▼                        ▼                        ▼
      ┌─────────────┐          ┌─────────────┐          ┌─────────────┐
      │ Subscriber  │          │ Subscriber  │          │ Subscriber  │
      │     A       │          │     B       │          │     C       │
      └─────────────┘          └─────────────┘          └─────────────┘

Testing

This package maintains comprehensive test coverage to ensure reliability and stability.

Test Categories

  • Initial signal value is 0
  • Returns all required properties (signal, emit, info)
  • Info object contains correct properties
  • Subscriber count tracking
  • Signal increments on emit
  • Multiple emits increment correctly
  • Emit count and timestamp update
  • Rapid successive emits
  • Data stored in info.data on emit
  • Data updates on each emit
  • Data shared across subscribers
  • Data ordering guarantee (available before signal changes)
  • Complex object data support
  • Debounced emit uses latest data
  • All subscribers receive signal updates
  • Subscriber count accuracy
  • Cleanup on unmount
  • emitOnMount behavior
  • onEmit callback execution
  • enabled option subscription control
  • debounce timing
  • Emit function stability
  • Info object stability
  • Values update with stable reference

License

MIT © mirunamu

This package is part of the usefy monorepo.