@usefy/use-signal 0.0.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,587 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/mirunamu00/usefy/master/assets/logo.png" alt="usefy logo" width="120" />
3
+ </p>
4
+
5
+ <h1 align="center">@usefy/use-signal</h1>
6
+
7
+ <p align="center">
8
+ <strong>A lightweight React hook for event-driven communication between components</strong>
9
+ </p>
10
+
11
+ <p align="center">
12
+ <a href="https://www.npmjs.com/package/@usefy/use-signal">
13
+ <img src="https://img.shields.io/npm/v/@usefy/use-signal.svg?style=flat-square&color=007acc" alt="npm version" />
14
+ </a>
15
+ <a href="https://www.npmjs.com/package/@usefy/use-signal">
16
+ <img src="https://img.shields.io/npm/dm/@usefy/use-signal.svg?style=flat-square&color=007acc" alt="npm downloads" />
17
+ </a>
18
+ <a href="https://bundlephobia.com/package/@usefy/use-signal">
19
+ <img src="https://img.shields.io/bundlephobia/minzip/@usefy/use-signal?style=flat-square&color=007acc" alt="bundle size" />
20
+ </a>
21
+ <a href="https://github.com/mirunamu00/usefy/blob/master/LICENSE">
22
+ <img src="https://img.shields.io/npm/l/@usefy/use-signal.svg?style=flat-square&color=007acc" alt="license" />
23
+ </a>
24
+ </p>
25
+
26
+ <p align="center">
27
+ <a href="#installation">Installation</a> •
28
+ <a href="#quick-start">Quick Start</a> •
29
+ <a href="#api-reference">API Reference</a> •
30
+ <a href="#examples">Examples</a> •
31
+ <a href="#license">License</a>
32
+ </p>
33
+
34
+ <p align="center">
35
+ <a href="https://mirunamu00.github.io/usefy/?path=/docs/hooks-usesignal--docs" target="_blank" rel="noopener noreferrer">
36
+ <strong>📚 View Storybook Demo</strong>
37
+ </a>
38
+ </p>
39
+
40
+ ---
41
+
42
+ ## Overview
43
+
44
+ `@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.
45
+
46
+ **Part of the [@usefy](https://www.npmjs.com/org/usefy) ecosystem** — a collection of production-ready React hooks designed for modern applications.
47
+
48
+ ### Why use-signal?
49
+
50
+ - **Zero Dependencies** — Pure React implementation with no external dependencies
51
+ - **TypeScript First** — Full type safety with exported interfaces
52
+ - **Stable References** — `emit` and `info` maintain stable references across re-renders
53
+ - **SSR Compatible** — Works seamlessly with Next.js, Remix, and other SSR frameworks
54
+ - **Lightweight** — Minimal bundle footprint (~1KB minified + gzipped)
55
+ - **Well Tested** — Comprehensive test coverage with Vitest
56
+ - **React 18+ Optimized** — Uses `useSyncExternalStore` for concurrent mode compatibility
57
+
58
+ ### Use Cases
59
+
60
+ - **Dashboard Refresh** — Refresh multiple widgets with a single button click
61
+ - **Form Reset** — Reset multiple form sections simultaneously
62
+ - **Cache Invalidation** — Invalidate and reload data across components
63
+ - **Multi-step Flows** — Coordinate state across wizard steps
64
+ - **Event Broadcasting** — Notify multiple listeners about system events
65
+
66
+ ### ⚠️ What This Hook Is NOT
67
+
68
+ **`useSignal` is NOT a global state management solution.**
69
+
70
+ This hook is designed for lightweight event-driven communication—sharing simple "signals" between components without the overhead of complex state management setup.
71
+
72
+ If you need:
73
+ - Complex shared state with derived values
74
+ - Persistent state across page navigation
75
+ - State that drives business logic
76
+ - Fine-grained state updates with selectors
77
+
78
+ → Use dedicated state management tools like **React Context**, **Zustand**, **Jotai**, **Recoil**, or **Redux**.
79
+
80
+ **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.
81
+
82
+ ```tsx
83
+ // ✅ Good: Signal with contextual data
84
+ emit({ itemId: "123", action: "refresh" });
85
+
86
+ // ❌ Bad: Using as global state
87
+ emit({ user: userData, cart: cartItems, settings: appSettings });
88
+ ```
89
+
90
+ ---
91
+
92
+ ## Installation
93
+
94
+ ```bash
95
+ # npm
96
+ npm install @usefy/use-signal
97
+
98
+ # yarn
99
+ yarn add @usefy/use-signal
100
+
101
+ # pnpm
102
+ pnpm add @usefy/use-signal
103
+ ```
104
+
105
+ ### Peer Dependencies
106
+
107
+ This package requires React 18 or 19:
108
+
109
+ ```json
110
+ {
111
+ "peerDependencies": {
112
+ "react": "^18.0.0 || ^19.0.0"
113
+ }
114
+ }
115
+ ```
116
+
117
+ ---
118
+
119
+ ## Quick Start
120
+
121
+ ```tsx
122
+ import { useSignal } from "@usefy/use-signal";
123
+ import { useEffect } from "react";
124
+
125
+ // Emitter Component
126
+ function RefreshButton() {
127
+ const { emit } = useSignal("dashboard-refresh");
128
+
129
+ return <button onClick={emit}>Refresh Dashboard</button>;
130
+ }
131
+
132
+ // Subscriber Component
133
+ function DataWidget() {
134
+ const { signal } = useSignal("dashboard-refresh");
135
+
136
+ useEffect(() => {
137
+ fetchData(); // Refetch when signal changes
138
+ }, [signal]);
139
+
140
+ return <div>Widget Content</div>;
141
+ }
142
+ ```
143
+
144
+ ---
145
+
146
+ ## API Reference
147
+
148
+ ### `useSignal(name, options?)`
149
+
150
+ A hook that subscribes to a named signal and provides emit functionality.
151
+
152
+ #### Parameters
153
+
154
+ | Parameter | Type | Description |
155
+ | --------- | --------------- | ---------------------------------- |
156
+ | `name` | `string` | Unique identifier for the signal |
157
+ | `options` | `SignalOptions` | Optional configuration (see below) |
158
+
159
+ #### Options
160
+
161
+ ```typescript
162
+ interface SignalOptions {
163
+ emitOnMount?: boolean; // Emit when component mounts (default: false)
164
+ onEmit?: () => void; // Callback executed on emit
165
+ enabled?: boolean; // Enable/disable subscription (default: true)
166
+ debounce?: number; // Debounce emit calls in milliseconds
167
+ }
168
+ ```
169
+
170
+ #### Returns `UseSignalReturn<T>`
171
+
172
+ | Property | Type | Description |
173
+ | -------- | ----------------- | ------------------------------------------- |
174
+ | `signal` | `number` | Current version number (use in deps arrays) |
175
+ | `emit` | `(data?: T) => void` | Function to emit the signal with optional data |
176
+ | `info` | `SignalInfo<T>` | Metadata object (see below) |
177
+
178
+ #### SignalInfo
179
+
180
+ ```typescript
181
+ interface SignalInfo<T = unknown> {
182
+ name: string; // Signal name
183
+ subscriberCount: number; // Active subscribers
184
+ timestamp: number; // Last emit timestamp
185
+ emitCount: number; // Total emit count
186
+ data: T | undefined; // Data passed with last emit
187
+ }
188
+ ```
189
+
190
+ > **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`.
191
+
192
+ ---
193
+
194
+ ## Examples
195
+
196
+ ### Dashboard Refresh Pattern
197
+
198
+ ```tsx
199
+ import { useSignal } from "@usefy/use-signal";
200
+ import { useEffect, useState } from "react";
201
+
202
+ function RefreshButton() {
203
+ const { emit, info } = useSignal("dashboard-refresh");
204
+
205
+ return (
206
+ <button onClick={emit}>
207
+ Refresh All ({info.subscriberCount} widgets)
208
+ </button>
209
+ );
210
+ }
211
+
212
+ function SalesChart() {
213
+ const { signal } = useSignal("dashboard-refresh");
214
+ const [data, setData] = useState([]);
215
+
216
+ useEffect(() => {
217
+ fetchSalesData().then(setData);
218
+ }, [signal]);
219
+
220
+ return <Chart data={data} />;
221
+ }
222
+
223
+ function UserStats() {
224
+ const { signal } = useSignal("dashboard-refresh");
225
+ const [stats, setStats] = useState(null);
226
+
227
+ useEffect(() => {
228
+ fetchUserStats().then(setStats);
229
+ }, [signal]);
230
+
231
+ return <Stats data={stats} />;
232
+ }
233
+ ```
234
+
235
+ ### Form Reset
236
+
237
+ ```tsx
238
+ import { useSignal } from "@usefy/use-signal";
239
+
240
+ function ResetButton() {
241
+ const { emit } = useSignal("form-reset");
242
+ return <button onClick={emit}>Reset All Fields</button>;
243
+ }
244
+
245
+ function PersonalInfoSection() {
246
+ const { signal } = useSignal("form-reset");
247
+ const [name, setName] = useState("");
248
+ const [email, setEmail] = useState("");
249
+
250
+ useEffect(() => {
251
+ if (signal > 0) {
252
+ setName("");
253
+ setEmail("");
254
+ }
255
+ }, [signal]);
256
+
257
+ return (
258
+ <section>
259
+ <input value={name} onChange={(e) => setName(e.target.value)} />
260
+ <input value={email} onChange={(e) => setEmail(e.target.value)} />
261
+ </section>
262
+ );
263
+ }
264
+ ```
265
+
266
+ ### With Data Payload
267
+
268
+ ```tsx
269
+ import { useSignal } from "@usefy/use-signal";
270
+ import { useEffect } from "react";
271
+
272
+ interface NotificationData {
273
+ type: "success" | "error" | "info";
274
+ message: string;
275
+ }
276
+
277
+ function NotificationEmitter() {
278
+ const { emit } = useSignal<NotificationData>("notification");
279
+
280
+ return (
281
+ <button
282
+ onClick={() =>
283
+ emit({ type: "success", message: "Operation completed!" })
284
+ }
285
+ >
286
+ Send Notification
287
+ </button>
288
+ );
289
+ }
290
+
291
+ function NotificationReceiver() {
292
+ const { signal, info } = useSignal<NotificationData>("notification");
293
+
294
+ useEffect(() => {
295
+ if (signal > 0 && info.data) {
296
+ // info.data is guaranteed to contain the latest data
297
+ console.log(`[${info.data.type}] ${info.data.message}`);
298
+ }
299
+ }, [signal]);
300
+
301
+ return <div>Last: {info.data?.message ?? "No notifications"}</div>;
302
+ }
303
+ ```
304
+
305
+ ### With Debounce
306
+
307
+ ```tsx
308
+ import { useSignal } from "@usefy/use-signal";
309
+
310
+ function SearchInput() {
311
+ const { emit } = useSignal<string>("search-update", { debounce: 300 });
312
+
313
+ return (
314
+ <input
315
+ type="text"
316
+ onChange={(e) => {
317
+ emit(e.target.value); // Debounced - uses latest value after 300ms
318
+ }}
319
+ />
320
+ );
321
+ }
322
+ ```
323
+
324
+ ### Conditional Subscription
325
+
326
+ ```tsx
327
+ import { useSignal } from "@usefy/use-signal";
328
+
329
+ function ConditionalWidget({ visible }: { visible: boolean }) {
330
+ // Only subscribe when visible
331
+ const { signal } = useSignal("updates", { enabled: visible });
332
+
333
+ useEffect(() => {
334
+ if (signal > 0) {
335
+ refreshData();
336
+ }
337
+ }, [signal]);
338
+
339
+ if (!visible) return null;
340
+ return <div>Widget Content</div>;
341
+ }
342
+ ```
343
+
344
+ ### With onEmit Callback
345
+
346
+ ```tsx
347
+ import { useSignal } from "@usefy/use-signal";
348
+
349
+ function LoggingEmitter() {
350
+ const { emit } = useSignal("analytics-event", {
351
+ onEmit: () => {
352
+ console.log("Event emitted at", new Date().toISOString());
353
+ trackAnalytics("signal_emitted");
354
+ },
355
+ });
356
+
357
+ return <button onClick={emit}>Track Event</button>;
358
+ }
359
+ ```
360
+
361
+ ### Emit on Mount
362
+
363
+ ```tsx
364
+ import { useSignal } from "@usefy/use-signal";
365
+
366
+ function AutoRefreshComponent() {
367
+ const { signal } = useSignal("data-refresh", { emitOnMount: true });
368
+
369
+ useEffect(() => {
370
+ // This runs immediately on mount due to emitOnMount
371
+ fetchLatestData();
372
+ }, [signal]);
373
+
374
+ return <div>Auto-refreshed content</div>;
375
+ }
376
+ ```
377
+
378
+ ### Using Info for Conditional Logic
379
+
380
+ ```tsx
381
+ import { useSignal } from "@usefy/use-signal";
382
+
383
+ function SmartEmitter() {
384
+ const { emit, info } = useSignal("notification");
385
+
386
+ const handleClick = () => {
387
+ // Only emit if there are subscribers
388
+ if (info.subscriberCount > 0) {
389
+ emit();
390
+ } else {
391
+ console.log("No subscribers, skipping emit");
392
+ }
393
+ };
394
+
395
+ return (
396
+ <button onClick={handleClick}>
397
+ Notify ({info.subscriberCount} listeners)
398
+ </button>
399
+ );
400
+ }
401
+ ```
402
+
403
+ ---
404
+
405
+ ## TypeScript
406
+
407
+ This hook is written in TypeScript and exports all interfaces. Use generics to type the data payload.
408
+
409
+ ```tsx
410
+ import {
411
+ useSignal,
412
+ type UseSignalReturn,
413
+ type SignalOptions,
414
+ type SignalInfo,
415
+ } from "@usefy/use-signal";
416
+
417
+ // With typed data payload
418
+ interface MyEventData {
419
+ action: string;
420
+ payload: Record<string, unknown>;
421
+ }
422
+
423
+ const { signal, emit, info }: UseSignalReturn<MyEventData> = useSignal<MyEventData>(
424
+ "my-signal",
425
+ {
426
+ emitOnMount: true,
427
+ onEmit: () => console.log("emitted"),
428
+ enabled: true,
429
+ debounce: 100,
430
+ }
431
+ );
432
+
433
+ // signal: number
434
+ // emit: (data?: MyEventData) => void
435
+ // info: SignalInfo<MyEventData>
436
+ // info.data: MyEventData | undefined
437
+ ```
438
+
439
+ ---
440
+
441
+ ## Performance
442
+
443
+ ### Stable References
444
+
445
+ The `emit` function and `info` object maintain stable references across re-renders:
446
+
447
+ ```tsx
448
+ const { emit, info } = useSignal<MyData>("my-signal");
449
+
450
+ // emit reference remains stable
451
+ useEffect(() => {
452
+ // Safe to use as dependencies
453
+ }, [emit]);
454
+
455
+ // info is a ref-based object - stable reference, live values via getters
456
+ console.log(info.subscriberCount); // Always current
457
+ console.log(info.data); // Latest data from last emit
458
+ ```
459
+
460
+ ### Data Ordering Guarantee
461
+
462
+ When `emit(data)` is called, the data is stored **before** the signal version increments:
463
+
464
+ ```tsx
465
+ const { signal, info } = useSignal<string>("my-signal");
466
+
467
+ useEffect(() => {
468
+ // This is guaranteed: info.data contains the data passed to emit()
469
+ // that triggered this signal change
470
+ console.log(info.data); // Always the latest data
471
+ }, [signal]);
472
+ ```
473
+
474
+ ### Minimal Re-renders
475
+
476
+ - Only subscribed components re-render when signal is emitted
477
+ - The signal value is a primitive number, optimized for dependency arrays
478
+ - Uses React 18's `useSyncExternalStore` for optimal concurrent mode support
479
+
480
+ ---
481
+
482
+ ## How It Works
483
+
484
+ 1. **Global Store**: A singleton Map stores signal data (version, subscribers, metadata, payload data)
485
+ 2. **Subscription**: Components subscribe via `useSyncExternalStore`
486
+ 3. **Emit**: Sets data → Increments version → Updates timestamp → Notifies all subscribers
487
+ 4. **Cleanup**: Automatic unsubscription on unmount
488
+
489
+ > **Key Design**: Data is set **before** version increment to ensure `useEffect` callbacks always see the latest `info.data`.
490
+
491
+ ```
492
+ ┌─────────────┐ emit() ┌─────────────┐
493
+ │ Component │ ───────────────▶│ Signal │
494
+ │ Emitter │ │ Store │
495
+ └─────────────┘ └──────┬──────┘
496
+
497
+ notify all subscribers
498
+
499
+ ┌────────────────────────┼────────────────────────┐
500
+ ▼ ▼ ▼
501
+ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
502
+ │ Subscriber │ │ Subscriber │ │ Subscriber │
503
+ │ A │ │ B │ │ C │
504
+ └─────────────┘ └─────────────┘ └─────────────┘
505
+ ```
506
+
507
+ ---
508
+
509
+ ## Testing
510
+
511
+ This package maintains comprehensive test coverage to ensure reliability and stability.
512
+
513
+ ### Test Categories
514
+
515
+ <details>
516
+ <summary><strong>Initialization Tests</strong></summary>
517
+
518
+ - Initial signal value is 0
519
+ - Returns all required properties (signal, emit, info)
520
+ - Info object contains correct properties
521
+ - Subscriber count tracking
522
+
523
+ </details>
524
+
525
+ <details>
526
+ <summary><strong>Emit Tests</strong></summary>
527
+
528
+ - Signal increments on emit
529
+ - Multiple emits increment correctly
530
+ - Emit count and timestamp update
531
+ - Rapid successive emits
532
+
533
+ </details>
534
+
535
+ <details>
536
+ <summary><strong>Data Payload Tests</strong></summary>
537
+
538
+ - Data stored in info.data on emit
539
+ - Data updates on each emit
540
+ - Data shared across subscribers
541
+ - Data ordering guarantee (available before signal changes)
542
+ - Complex object data support
543
+ - Debounced emit uses latest data
544
+
545
+ </details>
546
+
547
+ <details>
548
+ <summary><strong>Multi-Subscriber Tests</strong></summary>
549
+
550
+ - All subscribers receive signal updates
551
+ - Subscriber count accuracy
552
+ - Cleanup on unmount
553
+
554
+ </details>
555
+
556
+ <details>
557
+ <summary><strong>Options Tests</strong></summary>
558
+
559
+ - emitOnMount behavior
560
+ - onEmit callback execution
561
+ - enabled option subscription control
562
+ - debounce timing
563
+
564
+ </details>
565
+
566
+ <details>
567
+ <summary><strong>Stable Reference Tests</strong></summary>
568
+
569
+ - Emit function stability
570
+ - Info object stability
571
+ - Values update with stable reference
572
+
573
+ </details>
574
+
575
+ ---
576
+
577
+ ## License
578
+
579
+ MIT © [mirunamu](https://github.com/mirunamu00)
580
+
581
+ This package is part of the [usefy](https://github.com/mirunamu00/usefy) monorepo.
582
+
583
+ ---
584
+
585
+ <p align="center">
586
+ <sub>Built with care by the usefy team</sub>
587
+ </p>
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Signal metadata object for debugging and monitoring
3
+ */
4
+ interface SignalInfo<T = unknown> {
5
+ /** Signal subscription name */
6
+ name: string;
7
+ /** Current number of active subscribers */
8
+ subscriberCount: number;
9
+ /** Timestamp of last emit (Date.now()) */
10
+ timestamp: number;
11
+ /** Total number of times this signal has been emitted */
12
+ emitCount: number;
13
+ /** Data passed with the last emit */
14
+ data: T | undefined;
15
+ }
16
+ /**
17
+ * Options for useSignal hook
18
+ */
19
+ interface SignalOptions {
20
+ /** Automatically emit when component mounts */
21
+ emitOnMount?: boolean;
22
+ /** Callback executed when emit is called */
23
+ onEmit?: () => void;
24
+ /** Conditionally enable/disable subscription (default: true) */
25
+ enabled?: boolean;
26
+ /** Debounce emit calls in milliseconds */
27
+ debounce?: number;
28
+ }
29
+ /**
30
+ * Return type for useSignal hook
31
+ */
32
+ interface UseSignalReturn<T = unknown> {
33
+ /** Current signal version number - use in dependency arrays */
34
+ signal: number;
35
+ /** Function to emit the signal and notify all subscribers, optionally with data */
36
+ emit: (data?: T) => void;
37
+ /** Stable metadata object for debugging and monitoring */
38
+ info: SignalInfo<T>;
39
+ }
40
+ /**
41
+ * A hook for event-driven communication between components without prop drilling.
42
+ * Components subscribe to a shared signal by name. When any component emits,
43
+ * all subscribers receive a new version number.
44
+ *
45
+ * @param name - Unique identifier string for the signal channel
46
+ * @param options - Configuration options
47
+ * @returns Object containing signal value, emit function, and info metadata
48
+ *
49
+ * @example
50
+ * ```tsx
51
+ * // Parent Component - emits signal
52
+ * function ParentComponent() {
53
+ * const { emit } = useSignal("Dashboard Refresh");
54
+ *
55
+ * return <button onClick={emit}>Refresh All</button>;
56
+ * }
57
+ *
58
+ * // Child Component - subscribes to signal
59
+ * function DataTable() {
60
+ * const { signal } = useSignal("Dashboard Refresh");
61
+ *
62
+ * useEffect(() => {
63
+ * fetchTableData();
64
+ * }, [signal]);
65
+ *
66
+ * return <table>...</table>;
67
+ * }
68
+ * ```
69
+ *
70
+ * @example
71
+ * ```tsx
72
+ * // With debugging info
73
+ * function MonitoredComponent() {
74
+ * const { signal, emit, info } = useSignal("API Sync", {
75
+ * onEmit: () => console.log("Syncing...")
76
+ * });
77
+ *
78
+ * useEffect(() => {
79
+ * syncData();
80
+ * console.log(`Sync #${info.emitCount} with ${info.subscriberCount} listeners`);
81
+ * }, [signal]);
82
+ *
83
+ * return <button onClick={emit}>Sync</button>;
84
+ * }
85
+ * ```
86
+ *
87
+ * @example
88
+ * ```tsx
89
+ * // With data payload
90
+ * function DataEmitter() {
91
+ * const { emit } = useSignal<{ userId: string }>("user-action");
92
+ *
93
+ * const handleClick = (userId: string) => {
94
+ * emit({ userId });
95
+ * };
96
+ *
97
+ * return <button onClick={() => handleClick("123")}>Action</button>;
98
+ * }
99
+ *
100
+ * function DataReceiver() {
101
+ * const { signal, info } = useSignal<{ userId: string }>("user-action");
102
+ *
103
+ * useEffect(() => {
104
+ * if (info.data) {
105
+ * console.log("User action for:", info.data.userId);
106
+ * }
107
+ * }, [signal]);
108
+ *
109
+ * return <div>Listening...</div>;
110
+ * }
111
+ * ```
112
+ */
113
+ declare function useSignal<T = unknown>(name: string, options?: SignalOptions): UseSignalReturn<T>;
114
+
115
+ export { type SignalInfo, type SignalOptions, type UseSignalReturn, useSignal };
@@ -0,0 +1,115 @@
1
+ /**
2
+ * Signal metadata object for debugging and monitoring
3
+ */
4
+ interface SignalInfo<T = unknown> {
5
+ /** Signal subscription name */
6
+ name: string;
7
+ /** Current number of active subscribers */
8
+ subscriberCount: number;
9
+ /** Timestamp of last emit (Date.now()) */
10
+ timestamp: number;
11
+ /** Total number of times this signal has been emitted */
12
+ emitCount: number;
13
+ /** Data passed with the last emit */
14
+ data: T | undefined;
15
+ }
16
+ /**
17
+ * Options for useSignal hook
18
+ */
19
+ interface SignalOptions {
20
+ /** Automatically emit when component mounts */
21
+ emitOnMount?: boolean;
22
+ /** Callback executed when emit is called */
23
+ onEmit?: () => void;
24
+ /** Conditionally enable/disable subscription (default: true) */
25
+ enabled?: boolean;
26
+ /** Debounce emit calls in milliseconds */
27
+ debounce?: number;
28
+ }
29
+ /**
30
+ * Return type for useSignal hook
31
+ */
32
+ interface UseSignalReturn<T = unknown> {
33
+ /** Current signal version number - use in dependency arrays */
34
+ signal: number;
35
+ /** Function to emit the signal and notify all subscribers, optionally with data */
36
+ emit: (data?: T) => void;
37
+ /** Stable metadata object for debugging and monitoring */
38
+ info: SignalInfo<T>;
39
+ }
40
+ /**
41
+ * A hook for event-driven communication between components without prop drilling.
42
+ * Components subscribe to a shared signal by name. When any component emits,
43
+ * all subscribers receive a new version number.
44
+ *
45
+ * @param name - Unique identifier string for the signal channel
46
+ * @param options - Configuration options
47
+ * @returns Object containing signal value, emit function, and info metadata
48
+ *
49
+ * @example
50
+ * ```tsx
51
+ * // Parent Component - emits signal
52
+ * function ParentComponent() {
53
+ * const { emit } = useSignal("Dashboard Refresh");
54
+ *
55
+ * return <button onClick={emit}>Refresh All</button>;
56
+ * }
57
+ *
58
+ * // Child Component - subscribes to signal
59
+ * function DataTable() {
60
+ * const { signal } = useSignal("Dashboard Refresh");
61
+ *
62
+ * useEffect(() => {
63
+ * fetchTableData();
64
+ * }, [signal]);
65
+ *
66
+ * return <table>...</table>;
67
+ * }
68
+ * ```
69
+ *
70
+ * @example
71
+ * ```tsx
72
+ * // With debugging info
73
+ * function MonitoredComponent() {
74
+ * const { signal, emit, info } = useSignal("API Sync", {
75
+ * onEmit: () => console.log("Syncing...")
76
+ * });
77
+ *
78
+ * useEffect(() => {
79
+ * syncData();
80
+ * console.log(`Sync #${info.emitCount} with ${info.subscriberCount} listeners`);
81
+ * }, [signal]);
82
+ *
83
+ * return <button onClick={emit}>Sync</button>;
84
+ * }
85
+ * ```
86
+ *
87
+ * @example
88
+ * ```tsx
89
+ * // With data payload
90
+ * function DataEmitter() {
91
+ * const { emit } = useSignal<{ userId: string }>("user-action");
92
+ *
93
+ * const handleClick = (userId: string) => {
94
+ * emit({ userId });
95
+ * };
96
+ *
97
+ * return <button onClick={() => handleClick("123")}>Action</button>;
98
+ * }
99
+ *
100
+ * function DataReceiver() {
101
+ * const { signal, info } = useSignal<{ userId: string }>("user-action");
102
+ *
103
+ * useEffect(() => {
104
+ * if (info.data) {
105
+ * console.log("User action for:", info.data.userId);
106
+ * }
107
+ * }, [signal]);
108
+ *
109
+ * return <div>Listening...</div>;
110
+ * }
111
+ * ```
112
+ */
113
+ declare function useSignal<T = unknown>(name: string, options?: SignalOptions): UseSignalReturn<T>;
114
+
115
+ export { type SignalInfo, type SignalOptions, type UseSignalReturn, useSignal };
package/dist/index.js ADDED
@@ -0,0 +1,182 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ useSignal: () => useSignal
24
+ });
25
+ module.exports = __toCommonJS(index_exports);
26
+
27
+ // src/useSignal.ts
28
+ var import_react = require("react");
29
+
30
+ // src/store.ts
31
+ var signalStore = /* @__PURE__ */ new Map();
32
+ function getOrCreateSignal(name) {
33
+ if (!signalStore.has(name)) {
34
+ signalStore.set(name, {
35
+ version: 0,
36
+ subscribers: /* @__PURE__ */ new Set(),
37
+ emitCount: 0,
38
+ timestamp: 0,
39
+ data: void 0
40
+ });
41
+ }
42
+ return signalStore.get(name);
43
+ }
44
+ function subscribe(name, listener) {
45
+ const signal = getOrCreateSignal(name);
46
+ signal.subscribers.add(listener);
47
+ return () => {
48
+ signal.subscribers.delete(listener);
49
+ if (signal.subscribers.size === 0 && signal.emitCount === 0) {
50
+ signalStore.delete(name);
51
+ }
52
+ };
53
+ }
54
+ function getSnapshot(name) {
55
+ const signal = signalStore.get(name);
56
+ return signal?.version ?? 0;
57
+ }
58
+ function emit(name, data) {
59
+ const signal = getOrCreateSignal(name);
60
+ signal.data = data;
61
+ signal.version += 1;
62
+ signal.emitCount += 1;
63
+ signal.timestamp = Date.now();
64
+ signal.subscribers.forEach((listener) => listener());
65
+ }
66
+ function getSubscriberCount(name) {
67
+ return signalStore.get(name)?.subscribers.size ?? 0;
68
+ }
69
+ function getEmitCount(name) {
70
+ return signalStore.get(name)?.emitCount ?? 0;
71
+ }
72
+ function getTimestamp(name) {
73
+ return signalStore.get(name)?.timestamp ?? 0;
74
+ }
75
+ function getData(name) {
76
+ return signalStore.get(name)?.data;
77
+ }
78
+
79
+ // src/useSignal.ts
80
+ function useSignal(name, options = {}) {
81
+ const {
82
+ emitOnMount = false,
83
+ onEmit,
84
+ enabled = true,
85
+ debounce
86
+ } = options;
87
+ const onEmitRef = (0, import_react.useRef)(onEmit);
88
+ onEmitRef.current = onEmit;
89
+ const nameRef = (0, import_react.useRef)(name);
90
+ nameRef.current = name;
91
+ const infoRef = (0, import_react.useRef)(null);
92
+ if (!infoRef.current) {
93
+ infoRef.current = {
94
+ get name() {
95
+ return nameRef.current;
96
+ },
97
+ get subscriberCount() {
98
+ return getSubscriberCount(nameRef.current);
99
+ },
100
+ get timestamp() {
101
+ return getTimestamp(nameRef.current);
102
+ },
103
+ get emitCount() {
104
+ return getEmitCount(nameRef.current);
105
+ },
106
+ get data() {
107
+ return getData(nameRef.current);
108
+ }
109
+ };
110
+ }
111
+ const subscribeToStore = (0, import_react.useCallback)(
112
+ (onStoreChange) => {
113
+ if (!enabled) {
114
+ return () => {
115
+ };
116
+ }
117
+ return subscribe(name, onStoreChange);
118
+ },
119
+ [name, enabled]
120
+ );
121
+ const getSnapshot2 = (0, import_react.useCallback)(() => {
122
+ if (!enabled) {
123
+ return 0;
124
+ }
125
+ return getSnapshot(name);
126
+ }, [name, enabled]);
127
+ const getServerSnapshot = (0, import_react.useCallback)(() => {
128
+ return 0;
129
+ }, []);
130
+ const signal = (0, import_react.useSyncExternalStore)(
131
+ subscribeToStore,
132
+ getSnapshot2,
133
+ getServerSnapshot
134
+ );
135
+ const baseEmit = (0, import_react.useCallback)(
136
+ (data) => {
137
+ emit(name, data);
138
+ onEmitRef.current?.();
139
+ },
140
+ [name]
141
+ );
142
+ const debounceTimerRef = (0, import_react.useRef)(null);
143
+ const pendingDataRef = (0, import_react.useRef)(void 0);
144
+ const emit2 = (0, import_react.useMemo)(() => {
145
+ if (!debounce || debounce <= 0) {
146
+ return baseEmit;
147
+ }
148
+ return (data) => {
149
+ pendingDataRef.current = data;
150
+ if (debounceTimerRef.current) {
151
+ clearTimeout(debounceTimerRef.current);
152
+ }
153
+ debounceTimerRef.current = setTimeout(() => {
154
+ baseEmit(pendingDataRef.current);
155
+ pendingDataRef.current = void 0;
156
+ debounceTimerRef.current = null;
157
+ }, debounce);
158
+ };
159
+ }, [baseEmit, debounce]);
160
+ (0, import_react.useEffect)(() => {
161
+ return () => {
162
+ if (debounceTimerRef.current) {
163
+ clearTimeout(debounceTimerRef.current);
164
+ }
165
+ };
166
+ }, []);
167
+ (0, import_react.useEffect)(() => {
168
+ if (emitOnMount) {
169
+ baseEmit();
170
+ }
171
+ }, [emitOnMount, baseEmit]);
172
+ return {
173
+ signal,
174
+ emit: emit2,
175
+ info: infoRef.current
176
+ };
177
+ }
178
+ // Annotate the CommonJS export names for ESM import in node:
179
+ 0 && (module.exports = {
180
+ useSignal
181
+ });
182
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/useSignal.ts","../src/store.ts"],"sourcesContent":["export {\n useSignal,\n type UseSignalReturn,\n type SignalOptions,\n type SignalInfo,\n} from \"./useSignal\";\n","import {\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useSyncExternalStore,\n} from \"react\";\nimport {\n subscribe,\n getSnapshot as getStoreSnapshot,\n emit as storeEmit,\n getSubscriberCount,\n getEmitCount,\n getTimestamp,\n getData,\n} from \"./store\";\n\n/**\n * Signal metadata object for debugging and monitoring\n */\nexport interface SignalInfo<T = unknown> {\n /** Signal subscription name */\n name: string;\n /** Current number of active subscribers */\n subscriberCount: number;\n /** Timestamp of last emit (Date.now()) */\n timestamp: number;\n /** Total number of times this signal has been emitted */\n emitCount: number;\n /** Data passed with the last emit */\n data: T | undefined;\n}\n\n/**\n * Options for useSignal hook\n */\nexport interface SignalOptions {\n /** Automatically emit when component mounts */\n emitOnMount?: boolean;\n /** Callback executed when emit is called */\n onEmit?: () => void;\n /** Conditionally enable/disable subscription (default: true) */\n enabled?: boolean;\n /** Debounce emit calls in milliseconds */\n debounce?: number;\n}\n\n/**\n * Return type for useSignal hook\n */\nexport interface UseSignalReturn<T = unknown> {\n /** Current signal version number - use in dependency arrays */\n signal: number;\n /** Function to emit the signal and notify all subscribers, optionally with data */\n emit: (data?: T) => void;\n /** Stable metadata object for debugging and monitoring */\n info: SignalInfo<T>;\n}\n\n/**\n * A hook for event-driven communication between components without prop drilling.\n * Components subscribe to a shared signal by name. When any component emits,\n * all subscribers receive a new version number.\n *\n * @param name - Unique identifier string for the signal channel\n * @param options - Configuration options\n * @returns Object containing signal value, emit function, and info metadata\n *\n * @example\n * ```tsx\n * // Parent Component - emits signal\n * function ParentComponent() {\n * const { emit } = useSignal(\"Dashboard Refresh\");\n *\n * return <button onClick={emit}>Refresh All</button>;\n * }\n *\n * // Child Component - subscribes to signal\n * function DataTable() {\n * const { signal } = useSignal(\"Dashboard Refresh\");\n *\n * useEffect(() => {\n * fetchTableData();\n * }, [signal]);\n *\n * return <table>...</table>;\n * }\n * ```\n *\n * @example\n * ```tsx\n * // With debugging info\n * function MonitoredComponent() {\n * const { signal, emit, info } = useSignal(\"API Sync\", {\n * onEmit: () => console.log(\"Syncing...\")\n * });\n *\n * useEffect(() => {\n * syncData();\n * console.log(`Sync #${info.emitCount} with ${info.subscriberCount} listeners`);\n * }, [signal]);\n *\n * return <button onClick={emit}>Sync</button>;\n * }\n * ```\n *\n * @example\n * ```tsx\n * // With data payload\n * function DataEmitter() {\n * const { emit } = useSignal<{ userId: string }>(\"user-action\");\n *\n * const handleClick = (userId: string) => {\n * emit({ userId });\n * };\n *\n * return <button onClick={() => handleClick(\"123\")}>Action</button>;\n * }\n *\n * function DataReceiver() {\n * const { signal, info } = useSignal<{ userId: string }>(\"user-action\");\n *\n * useEffect(() => {\n * if (info.data) {\n * console.log(\"User action for:\", info.data.userId);\n * }\n * }, [signal]);\n *\n * return <div>Listening...</div>;\n * }\n * ```\n */\nexport function useSignal<T = unknown>(\n name: string,\n options: SignalOptions = {}\n): UseSignalReturn<T> {\n const {\n emitOnMount = false,\n onEmit,\n enabled = true,\n debounce,\n } = options;\n\n // Store options in refs for stable references\n const onEmitRef = useRef(onEmit);\n onEmitRef.current = onEmit;\n\n // Stable name ref for info object\n const nameRef = useRef(name);\n nameRef.current = name;\n\n // Info object with stable reference using getters for live data\n const infoRef = useRef<SignalInfo<T> | null>(null);\n if (!infoRef.current) {\n infoRef.current = {\n get name() {\n return nameRef.current;\n },\n get subscriberCount() {\n return getSubscriberCount(nameRef.current);\n },\n get timestamp() {\n return getTimestamp(nameRef.current);\n },\n get emitCount() {\n return getEmitCount(nameRef.current);\n },\n get data() {\n return getData(nameRef.current) as T | undefined;\n },\n } as SignalInfo<T>;\n }\n\n // Subscribe function for useSyncExternalStore\n const subscribeToStore = useCallback(\n (onStoreChange: () => void) => {\n if (!enabled) {\n return () => {};\n }\n return subscribe(name, onStoreChange);\n },\n [name, enabled]\n );\n\n // Get snapshot function\n const getSnapshot = useCallback((): number => {\n if (!enabled) {\n return 0;\n }\n return getStoreSnapshot(name);\n }, [name, enabled]);\n\n // Server snapshot (always 0 for SSR)\n const getServerSnapshot = useCallback((): number => {\n return 0;\n }, []);\n\n // Use useSyncExternalStore for synchronized state\n const signal = useSyncExternalStore(\n subscribeToStore,\n getSnapshot,\n getServerSnapshot\n );\n\n // Base emit function\n const baseEmit = useCallback(\n (data?: T) => {\n storeEmit(name, data);\n onEmitRef.current?.();\n },\n [name]\n );\n\n // Debounce timer ref\n const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n // Store the latest data for debounced emit\n const pendingDataRef = useRef<T | undefined>(undefined);\n\n // Debounced or regular emit\n const emit = useMemo(() => {\n if (!debounce || debounce <= 0) {\n return baseEmit;\n }\n\n return (data?: T) => {\n // Store the latest data\n pendingDataRef.current = data;\n\n if (debounceTimerRef.current) {\n clearTimeout(debounceTimerRef.current);\n }\n debounceTimerRef.current = setTimeout(() => {\n baseEmit(pendingDataRef.current);\n pendingDataRef.current = undefined;\n debounceTimerRef.current = null;\n }, debounce);\n };\n }, [baseEmit, debounce]);\n\n // Cleanup debounce timer on unmount\n useEffect(() => {\n return () => {\n if (debounceTimerRef.current) {\n clearTimeout(debounceTimerRef.current);\n }\n };\n }, []);\n\n // Emit on mount if option is set\n useEffect(() => {\n if (emitOnMount) {\n baseEmit();\n }\n }, [emitOnMount, baseEmit]);\n\n return {\n signal,\n emit,\n info: infoRef.current!,\n };\n}\n","/**\n * Internal Signal Store for cross-component communication\n * This module manages signal versions and subscribers for the useSignal hook.\n *\n * @internal This module is not exported publicly\n */\n\n/** Signal data structure for each named signal */\ninterface SignalData {\n version: number;\n subscribers: Set<() => void>;\n emitCount: number;\n timestamp: number;\n data: unknown;\n}\n\n/** Map of signal name -> SignalData */\nconst signalStore = new Map<string, SignalData>();\n\n/**\n * Get or create signal data for a given name\n * @param name - The signal name\n * @returns SignalData object\n */\nfunction getOrCreateSignal(name: string): SignalData {\n if (!signalStore.has(name)) {\n signalStore.set(name, {\n version: 0,\n subscribers: new Set(),\n emitCount: 0,\n timestamp: 0,\n data: undefined,\n });\n }\n return signalStore.get(name)!;\n}\n\n/**\n * Subscribe a listener to changes for a specific signal name\n * @param name - The signal name to subscribe to\n * @param listener - Callback to invoke when the signal is emitted\n * @returns Unsubscribe function\n */\nexport function subscribe(name: string, listener: () => void): () => void {\n const signal = getOrCreateSignal(name);\n signal.subscribers.add(listener);\n\n return () => {\n signal.subscribers.delete(listener);\n\n // Cleanup: remove the signal entry if no more subscribers and never emitted\n if (signal.subscribers.size === 0 && signal.emitCount === 0) {\n signalStore.delete(name);\n }\n };\n}\n\n/**\n * Get the current version number for a signal\n * @param name - The signal name\n * @returns Current version number (0 if signal doesn't exist)\n */\nexport function getSnapshot(name: string): number {\n const signal = signalStore.get(name);\n return signal?.version ?? 0;\n}\n\n/**\n * Emit a signal - update data, increment version, update metadata, and notify all subscribers\n * Data is set BEFORE version increment to ensure useEffect callbacks see the latest data.\n * @param name - The signal name to emit\n * @param data - Optional data to pass with the signal\n */\nexport function emit(name: string, data?: unknown): void {\n const signal = getOrCreateSignal(name);\n\n // Set data FIRST before incrementing version\n // This ensures that when useEffect runs due to signal change,\n // info.data already contains the latest value\n signal.data = data;\n\n signal.version += 1;\n signal.emitCount += 1;\n signal.timestamp = Date.now();\n\n // Notify all subscribers\n signal.subscribers.forEach((listener) => listener());\n}\n\n/**\n * Get the current subscriber count for a signal\n * @param name - The signal name\n * @returns Number of active subscribers\n */\nexport function getSubscriberCount(name: string): number {\n return signalStore.get(name)?.subscribers.size ?? 0;\n}\n\n/**\n * Get the total emit count for a signal\n * @param name - The signal name\n * @returns Total number of times the signal has been emitted\n */\nexport function getEmitCount(name: string): number {\n return signalStore.get(name)?.emitCount ?? 0;\n}\n\n/**\n * Get the last emit timestamp for a signal\n * @param name - The signal name\n * @returns Timestamp of last emit (0 if never emitted)\n */\nexport function getTimestamp(name: string): number {\n return signalStore.get(name)?.timestamp ?? 0;\n}\n\n/**\n * Get the data passed with the last emit for a signal\n * @param name - The signal name\n * @returns Data from last emit (undefined if never emitted or no data)\n */\nexport function getData(name: string): unknown {\n return signalStore.get(name)?.data;\n}\n\n/**\n * Clear all signals (for testing purposes)\n * @internal\n */\nexport function clearAllSignals(): void {\n signalStore.clear();\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,mBAMO;;;ACWP,IAAM,cAAc,oBAAI,IAAwB;AAOhD,SAAS,kBAAkB,MAA0B;AACnD,MAAI,CAAC,YAAY,IAAI,IAAI,GAAG;AAC1B,gBAAY,IAAI,MAAM;AAAA,MACpB,SAAS;AAAA,MACT,aAAa,oBAAI,IAAI;AAAA,MACrB,WAAW;AAAA,MACX,WAAW;AAAA,MACX,MAAM;AAAA,IACR,CAAC;AAAA,EACH;AACA,SAAO,YAAY,IAAI,IAAI;AAC7B;AAQO,SAAS,UAAU,MAAc,UAAkC;AACxE,QAAM,SAAS,kBAAkB,IAAI;AACrC,SAAO,YAAY,IAAI,QAAQ;AAE/B,SAAO,MAAM;AACX,WAAO,YAAY,OAAO,QAAQ;AAGlC,QAAI,OAAO,YAAY,SAAS,KAAK,OAAO,cAAc,GAAG;AAC3D,kBAAY,OAAO,IAAI;AAAA,IACzB;AAAA,EACF;AACF;AAOO,SAAS,YAAY,MAAsB;AAChD,QAAM,SAAS,YAAY,IAAI,IAAI;AACnC,SAAO,QAAQ,WAAW;AAC5B;AAQO,SAAS,KAAK,MAAc,MAAsB;AACvD,QAAM,SAAS,kBAAkB,IAAI;AAKrC,SAAO,OAAO;AAEd,SAAO,WAAW;AAClB,SAAO,aAAa;AACpB,SAAO,YAAY,KAAK,IAAI;AAG5B,SAAO,YAAY,QAAQ,CAAC,aAAa,SAAS,CAAC;AACrD;AAOO,SAAS,mBAAmB,MAAsB;AACvD,SAAO,YAAY,IAAI,IAAI,GAAG,YAAY,QAAQ;AACpD;AAOO,SAAS,aAAa,MAAsB;AACjD,SAAO,YAAY,IAAI,IAAI,GAAG,aAAa;AAC7C;AAOO,SAAS,aAAa,MAAsB;AACjD,SAAO,YAAY,IAAI,IAAI,GAAG,aAAa;AAC7C;AAOO,SAAS,QAAQ,MAAuB;AAC7C,SAAO,YAAY,IAAI,IAAI,GAAG;AAChC;;;ADSO,SAAS,UACd,MACA,UAAyB,CAAC,GACN;AACpB,QAAM;AAAA,IACJ,cAAc;AAAA,IACd;AAAA,IACA,UAAU;AAAA,IACV;AAAA,EACF,IAAI;AAGJ,QAAM,gBAAY,qBAAO,MAAM;AAC/B,YAAU,UAAU;AAGpB,QAAM,cAAU,qBAAO,IAAI;AAC3B,UAAQ,UAAU;AAGlB,QAAM,cAAU,qBAA6B,IAAI;AACjD,MAAI,CAAC,QAAQ,SAAS;AACpB,YAAQ,UAAU;AAAA,MAChB,IAAI,OAAO;AACT,eAAO,QAAQ;AAAA,MACjB;AAAA,MACA,IAAI,kBAAkB;AACpB,eAAO,mBAAmB,QAAQ,OAAO;AAAA,MAC3C;AAAA,MACA,IAAI,YAAY;AACd,eAAO,aAAa,QAAQ,OAAO;AAAA,MACrC;AAAA,MACA,IAAI,YAAY;AACd,eAAO,aAAa,QAAQ,OAAO;AAAA,MACrC;AAAA,MACA,IAAI,OAAO;AACT,eAAO,QAAQ,QAAQ,OAAO;AAAA,MAChC;AAAA,IACF;AAAA,EACF;AAGA,QAAM,uBAAmB;AAAA,IACvB,CAAC,kBAA8B;AAC7B,UAAI,CAAC,SAAS;AACZ,eAAO,MAAM;AAAA,QAAC;AAAA,MAChB;AACA,aAAO,UAAU,MAAM,aAAa;AAAA,IACtC;AAAA,IACA,CAAC,MAAM,OAAO;AAAA,EAChB;AAGA,QAAMA,mBAAc,0BAAY,MAAc;AAC5C,QAAI,CAAC,SAAS;AACZ,aAAO;AAAA,IACT;AACA,WAAO,YAAiB,IAAI;AAAA,EAC9B,GAAG,CAAC,MAAM,OAAO,CAAC;AAGlB,QAAM,wBAAoB,0BAAY,MAAc;AAClD,WAAO;AAAA,EACT,GAAG,CAAC,CAAC;AAGL,QAAM,aAAS;AAAA,IACb;AAAA,IACAA;AAAA,IACA;AAAA,EACF;AAGA,QAAM,eAAW;AAAA,IACf,CAAC,SAAa;AACZ,WAAU,MAAM,IAAI;AACpB,gBAAU,UAAU;AAAA,IACtB;AAAA,IACA,CAAC,IAAI;AAAA,EACP;AAGA,QAAM,uBAAmB,qBAA6C,IAAI;AAE1E,QAAM,qBAAiB,qBAAsB,MAAS;AAGtD,QAAMC,YAAO,sBAAQ,MAAM;AACzB,QAAI,CAAC,YAAY,YAAY,GAAG;AAC9B,aAAO;AAAA,IACT;AAEA,WAAO,CAAC,SAAa;AAEnB,qBAAe,UAAU;AAEzB,UAAI,iBAAiB,SAAS;AAC5B,qBAAa,iBAAiB,OAAO;AAAA,MACvC;AACA,uBAAiB,UAAU,WAAW,MAAM;AAC1C,iBAAS,eAAe,OAAO;AAC/B,uBAAe,UAAU;AACzB,yBAAiB,UAAU;AAAA,MAC7B,GAAG,QAAQ;AAAA,IACb;AAAA,EACF,GAAG,CAAC,UAAU,QAAQ,CAAC;AAGvB,8BAAU,MAAM;AACd,WAAO,MAAM;AACX,UAAI,iBAAiB,SAAS;AAC5B,qBAAa,iBAAiB,OAAO;AAAA,MACvC;AAAA,IACF;AAAA,EACF,GAAG,CAAC,CAAC;AAGL,8BAAU,MAAM;AACd,QAAI,aAAa;AACf,eAAS;AAAA,IACX;AAAA,EACF,GAAG,CAAC,aAAa,QAAQ,CAAC;AAE1B,SAAO;AAAA,IACL;AAAA,IACA,MAAAA;AAAA,IACA,MAAM,QAAQ;AAAA,EAChB;AACF;","names":["getSnapshot","emit"]}
package/dist/index.mjs ADDED
@@ -0,0 +1,161 @@
1
+ // src/useSignal.ts
2
+ import {
3
+ useCallback,
4
+ useEffect,
5
+ useMemo,
6
+ useRef,
7
+ useSyncExternalStore
8
+ } from "react";
9
+
10
+ // src/store.ts
11
+ var signalStore = /* @__PURE__ */ new Map();
12
+ function getOrCreateSignal(name) {
13
+ if (!signalStore.has(name)) {
14
+ signalStore.set(name, {
15
+ version: 0,
16
+ subscribers: /* @__PURE__ */ new Set(),
17
+ emitCount: 0,
18
+ timestamp: 0,
19
+ data: void 0
20
+ });
21
+ }
22
+ return signalStore.get(name);
23
+ }
24
+ function subscribe(name, listener) {
25
+ const signal = getOrCreateSignal(name);
26
+ signal.subscribers.add(listener);
27
+ return () => {
28
+ signal.subscribers.delete(listener);
29
+ if (signal.subscribers.size === 0 && signal.emitCount === 0) {
30
+ signalStore.delete(name);
31
+ }
32
+ };
33
+ }
34
+ function getSnapshot(name) {
35
+ const signal = signalStore.get(name);
36
+ return signal?.version ?? 0;
37
+ }
38
+ function emit(name, data) {
39
+ const signal = getOrCreateSignal(name);
40
+ signal.data = data;
41
+ signal.version += 1;
42
+ signal.emitCount += 1;
43
+ signal.timestamp = Date.now();
44
+ signal.subscribers.forEach((listener) => listener());
45
+ }
46
+ function getSubscriberCount(name) {
47
+ return signalStore.get(name)?.subscribers.size ?? 0;
48
+ }
49
+ function getEmitCount(name) {
50
+ return signalStore.get(name)?.emitCount ?? 0;
51
+ }
52
+ function getTimestamp(name) {
53
+ return signalStore.get(name)?.timestamp ?? 0;
54
+ }
55
+ function getData(name) {
56
+ return signalStore.get(name)?.data;
57
+ }
58
+
59
+ // src/useSignal.ts
60
+ function useSignal(name, options = {}) {
61
+ const {
62
+ emitOnMount = false,
63
+ onEmit,
64
+ enabled = true,
65
+ debounce
66
+ } = options;
67
+ const onEmitRef = useRef(onEmit);
68
+ onEmitRef.current = onEmit;
69
+ const nameRef = useRef(name);
70
+ nameRef.current = name;
71
+ const infoRef = useRef(null);
72
+ if (!infoRef.current) {
73
+ infoRef.current = {
74
+ get name() {
75
+ return nameRef.current;
76
+ },
77
+ get subscriberCount() {
78
+ return getSubscriberCount(nameRef.current);
79
+ },
80
+ get timestamp() {
81
+ return getTimestamp(nameRef.current);
82
+ },
83
+ get emitCount() {
84
+ return getEmitCount(nameRef.current);
85
+ },
86
+ get data() {
87
+ return getData(nameRef.current);
88
+ }
89
+ };
90
+ }
91
+ const subscribeToStore = useCallback(
92
+ (onStoreChange) => {
93
+ if (!enabled) {
94
+ return () => {
95
+ };
96
+ }
97
+ return subscribe(name, onStoreChange);
98
+ },
99
+ [name, enabled]
100
+ );
101
+ const getSnapshot2 = useCallback(() => {
102
+ if (!enabled) {
103
+ return 0;
104
+ }
105
+ return getSnapshot(name);
106
+ }, [name, enabled]);
107
+ const getServerSnapshot = useCallback(() => {
108
+ return 0;
109
+ }, []);
110
+ const signal = useSyncExternalStore(
111
+ subscribeToStore,
112
+ getSnapshot2,
113
+ getServerSnapshot
114
+ );
115
+ const baseEmit = useCallback(
116
+ (data) => {
117
+ emit(name, data);
118
+ onEmitRef.current?.();
119
+ },
120
+ [name]
121
+ );
122
+ const debounceTimerRef = useRef(null);
123
+ const pendingDataRef = useRef(void 0);
124
+ const emit2 = useMemo(() => {
125
+ if (!debounce || debounce <= 0) {
126
+ return baseEmit;
127
+ }
128
+ return (data) => {
129
+ pendingDataRef.current = data;
130
+ if (debounceTimerRef.current) {
131
+ clearTimeout(debounceTimerRef.current);
132
+ }
133
+ debounceTimerRef.current = setTimeout(() => {
134
+ baseEmit(pendingDataRef.current);
135
+ pendingDataRef.current = void 0;
136
+ debounceTimerRef.current = null;
137
+ }, debounce);
138
+ };
139
+ }, [baseEmit, debounce]);
140
+ useEffect(() => {
141
+ return () => {
142
+ if (debounceTimerRef.current) {
143
+ clearTimeout(debounceTimerRef.current);
144
+ }
145
+ };
146
+ }, []);
147
+ useEffect(() => {
148
+ if (emitOnMount) {
149
+ baseEmit();
150
+ }
151
+ }, [emitOnMount, baseEmit]);
152
+ return {
153
+ signal,
154
+ emit: emit2,
155
+ info: infoRef.current
156
+ };
157
+ }
158
+ export {
159
+ useSignal
160
+ };
161
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/useSignal.ts","../src/store.ts"],"sourcesContent":["import {\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useSyncExternalStore,\n} from \"react\";\nimport {\n subscribe,\n getSnapshot as getStoreSnapshot,\n emit as storeEmit,\n getSubscriberCount,\n getEmitCount,\n getTimestamp,\n getData,\n} from \"./store\";\n\n/**\n * Signal metadata object for debugging and monitoring\n */\nexport interface SignalInfo<T = unknown> {\n /** Signal subscription name */\n name: string;\n /** Current number of active subscribers */\n subscriberCount: number;\n /** Timestamp of last emit (Date.now()) */\n timestamp: number;\n /** Total number of times this signal has been emitted */\n emitCount: number;\n /** Data passed with the last emit */\n data: T | undefined;\n}\n\n/**\n * Options for useSignal hook\n */\nexport interface SignalOptions {\n /** Automatically emit when component mounts */\n emitOnMount?: boolean;\n /** Callback executed when emit is called */\n onEmit?: () => void;\n /** Conditionally enable/disable subscription (default: true) */\n enabled?: boolean;\n /** Debounce emit calls in milliseconds */\n debounce?: number;\n}\n\n/**\n * Return type for useSignal hook\n */\nexport interface UseSignalReturn<T = unknown> {\n /** Current signal version number - use in dependency arrays */\n signal: number;\n /** Function to emit the signal and notify all subscribers, optionally with data */\n emit: (data?: T) => void;\n /** Stable metadata object for debugging and monitoring */\n info: SignalInfo<T>;\n}\n\n/**\n * A hook for event-driven communication between components without prop drilling.\n * Components subscribe to a shared signal by name. When any component emits,\n * all subscribers receive a new version number.\n *\n * @param name - Unique identifier string for the signal channel\n * @param options - Configuration options\n * @returns Object containing signal value, emit function, and info metadata\n *\n * @example\n * ```tsx\n * // Parent Component - emits signal\n * function ParentComponent() {\n * const { emit } = useSignal(\"Dashboard Refresh\");\n *\n * return <button onClick={emit}>Refresh All</button>;\n * }\n *\n * // Child Component - subscribes to signal\n * function DataTable() {\n * const { signal } = useSignal(\"Dashboard Refresh\");\n *\n * useEffect(() => {\n * fetchTableData();\n * }, [signal]);\n *\n * return <table>...</table>;\n * }\n * ```\n *\n * @example\n * ```tsx\n * // With debugging info\n * function MonitoredComponent() {\n * const { signal, emit, info } = useSignal(\"API Sync\", {\n * onEmit: () => console.log(\"Syncing...\")\n * });\n *\n * useEffect(() => {\n * syncData();\n * console.log(`Sync #${info.emitCount} with ${info.subscriberCount} listeners`);\n * }, [signal]);\n *\n * return <button onClick={emit}>Sync</button>;\n * }\n * ```\n *\n * @example\n * ```tsx\n * // With data payload\n * function DataEmitter() {\n * const { emit } = useSignal<{ userId: string }>(\"user-action\");\n *\n * const handleClick = (userId: string) => {\n * emit({ userId });\n * };\n *\n * return <button onClick={() => handleClick(\"123\")}>Action</button>;\n * }\n *\n * function DataReceiver() {\n * const { signal, info } = useSignal<{ userId: string }>(\"user-action\");\n *\n * useEffect(() => {\n * if (info.data) {\n * console.log(\"User action for:\", info.data.userId);\n * }\n * }, [signal]);\n *\n * return <div>Listening...</div>;\n * }\n * ```\n */\nexport function useSignal<T = unknown>(\n name: string,\n options: SignalOptions = {}\n): UseSignalReturn<T> {\n const {\n emitOnMount = false,\n onEmit,\n enabled = true,\n debounce,\n } = options;\n\n // Store options in refs for stable references\n const onEmitRef = useRef(onEmit);\n onEmitRef.current = onEmit;\n\n // Stable name ref for info object\n const nameRef = useRef(name);\n nameRef.current = name;\n\n // Info object with stable reference using getters for live data\n const infoRef = useRef<SignalInfo<T> | null>(null);\n if (!infoRef.current) {\n infoRef.current = {\n get name() {\n return nameRef.current;\n },\n get subscriberCount() {\n return getSubscriberCount(nameRef.current);\n },\n get timestamp() {\n return getTimestamp(nameRef.current);\n },\n get emitCount() {\n return getEmitCount(nameRef.current);\n },\n get data() {\n return getData(nameRef.current) as T | undefined;\n },\n } as SignalInfo<T>;\n }\n\n // Subscribe function for useSyncExternalStore\n const subscribeToStore = useCallback(\n (onStoreChange: () => void) => {\n if (!enabled) {\n return () => {};\n }\n return subscribe(name, onStoreChange);\n },\n [name, enabled]\n );\n\n // Get snapshot function\n const getSnapshot = useCallback((): number => {\n if (!enabled) {\n return 0;\n }\n return getStoreSnapshot(name);\n }, [name, enabled]);\n\n // Server snapshot (always 0 for SSR)\n const getServerSnapshot = useCallback((): number => {\n return 0;\n }, []);\n\n // Use useSyncExternalStore for synchronized state\n const signal = useSyncExternalStore(\n subscribeToStore,\n getSnapshot,\n getServerSnapshot\n );\n\n // Base emit function\n const baseEmit = useCallback(\n (data?: T) => {\n storeEmit(name, data);\n onEmitRef.current?.();\n },\n [name]\n );\n\n // Debounce timer ref\n const debounceTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);\n // Store the latest data for debounced emit\n const pendingDataRef = useRef<T | undefined>(undefined);\n\n // Debounced or regular emit\n const emit = useMemo(() => {\n if (!debounce || debounce <= 0) {\n return baseEmit;\n }\n\n return (data?: T) => {\n // Store the latest data\n pendingDataRef.current = data;\n\n if (debounceTimerRef.current) {\n clearTimeout(debounceTimerRef.current);\n }\n debounceTimerRef.current = setTimeout(() => {\n baseEmit(pendingDataRef.current);\n pendingDataRef.current = undefined;\n debounceTimerRef.current = null;\n }, debounce);\n };\n }, [baseEmit, debounce]);\n\n // Cleanup debounce timer on unmount\n useEffect(() => {\n return () => {\n if (debounceTimerRef.current) {\n clearTimeout(debounceTimerRef.current);\n }\n };\n }, []);\n\n // Emit on mount if option is set\n useEffect(() => {\n if (emitOnMount) {\n baseEmit();\n }\n }, [emitOnMount, baseEmit]);\n\n return {\n signal,\n emit,\n info: infoRef.current!,\n };\n}\n","/**\n * Internal Signal Store for cross-component communication\n * This module manages signal versions and subscribers for the useSignal hook.\n *\n * @internal This module is not exported publicly\n */\n\n/** Signal data structure for each named signal */\ninterface SignalData {\n version: number;\n subscribers: Set<() => void>;\n emitCount: number;\n timestamp: number;\n data: unknown;\n}\n\n/** Map of signal name -> SignalData */\nconst signalStore = new Map<string, SignalData>();\n\n/**\n * Get or create signal data for a given name\n * @param name - The signal name\n * @returns SignalData object\n */\nfunction getOrCreateSignal(name: string): SignalData {\n if (!signalStore.has(name)) {\n signalStore.set(name, {\n version: 0,\n subscribers: new Set(),\n emitCount: 0,\n timestamp: 0,\n data: undefined,\n });\n }\n return signalStore.get(name)!;\n}\n\n/**\n * Subscribe a listener to changes for a specific signal name\n * @param name - The signal name to subscribe to\n * @param listener - Callback to invoke when the signal is emitted\n * @returns Unsubscribe function\n */\nexport function subscribe(name: string, listener: () => void): () => void {\n const signal = getOrCreateSignal(name);\n signal.subscribers.add(listener);\n\n return () => {\n signal.subscribers.delete(listener);\n\n // Cleanup: remove the signal entry if no more subscribers and never emitted\n if (signal.subscribers.size === 0 && signal.emitCount === 0) {\n signalStore.delete(name);\n }\n };\n}\n\n/**\n * Get the current version number for a signal\n * @param name - The signal name\n * @returns Current version number (0 if signal doesn't exist)\n */\nexport function getSnapshot(name: string): number {\n const signal = signalStore.get(name);\n return signal?.version ?? 0;\n}\n\n/**\n * Emit a signal - update data, increment version, update metadata, and notify all subscribers\n * Data is set BEFORE version increment to ensure useEffect callbacks see the latest data.\n * @param name - The signal name to emit\n * @param data - Optional data to pass with the signal\n */\nexport function emit(name: string, data?: unknown): void {\n const signal = getOrCreateSignal(name);\n\n // Set data FIRST before incrementing version\n // This ensures that when useEffect runs due to signal change,\n // info.data already contains the latest value\n signal.data = data;\n\n signal.version += 1;\n signal.emitCount += 1;\n signal.timestamp = Date.now();\n\n // Notify all subscribers\n signal.subscribers.forEach((listener) => listener());\n}\n\n/**\n * Get the current subscriber count for a signal\n * @param name - The signal name\n * @returns Number of active subscribers\n */\nexport function getSubscriberCount(name: string): number {\n return signalStore.get(name)?.subscribers.size ?? 0;\n}\n\n/**\n * Get the total emit count for a signal\n * @param name - The signal name\n * @returns Total number of times the signal has been emitted\n */\nexport function getEmitCount(name: string): number {\n return signalStore.get(name)?.emitCount ?? 0;\n}\n\n/**\n * Get the last emit timestamp for a signal\n * @param name - The signal name\n * @returns Timestamp of last emit (0 if never emitted)\n */\nexport function getTimestamp(name: string): number {\n return signalStore.get(name)?.timestamp ?? 0;\n}\n\n/**\n * Get the data passed with the last emit for a signal\n * @param name - The signal name\n * @returns Data from last emit (undefined if never emitted or no data)\n */\nexport function getData(name: string): unknown {\n return signalStore.get(name)?.data;\n}\n\n/**\n * Clear all signals (for testing purposes)\n * @internal\n */\nexport function clearAllSignals(): void {\n signalStore.clear();\n}\n"],"mappings":"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","names":["getSnapshot","emit"]}
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@usefy/use-signal",
3
+ "version": "0.0.31",
4
+ "description": "A React hook for event-driven communication between components",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "sideEffects": false,
19
+ "peerDependencies": {
20
+ "react": "^18.0.0 || ^19.0.0"
21
+ },
22
+ "devDependencies": {
23
+ "@testing-library/jest-dom": "^6.9.1",
24
+ "@testing-library/react": "^16.3.1",
25
+ "@testing-library/user-event": "^14.6.1",
26
+ "@types/react": "^19.0.0",
27
+ "jsdom": "^27.3.0",
28
+ "react": "^19.0.0",
29
+ "rimraf": "^6.0.1",
30
+ "tsup": "^8.0.0",
31
+ "typescript": "^5.0.0",
32
+ "vitest": "^4.0.16"
33
+ },
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "https://github.com/mirunamu00/usefy.git",
40
+ "directory": "packages/use-signal"
41
+ },
42
+ "license": "MIT",
43
+ "keywords": [
44
+ "react",
45
+ "hooks",
46
+ "signal",
47
+ "event",
48
+ "broadcast",
49
+ "pubsub"
50
+ ],
51
+ "scripts": {
52
+ "build": "tsup",
53
+ "dev": "tsup --watch",
54
+ "test": "vitest run",
55
+ "test:watch": "vitest",
56
+ "typecheck": "tsc --noEmit",
57
+ "clean": "rimraf dist"
58
+ }
59
+ }