77 lines
2.5 KiB
TypeScript
77 lines
2.5 KiB
TypeScript
import { useEffect, useRef, useCallback } from 'react';
|
|
import type { AppEvents } from './events.registry';
|
|
import { eventBus, publish as busPublish } from './event-bus';
|
|
|
|
// ─── useAppEvent ────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Subscribe to an application event with automatic cleanup on unmount.
|
|
*
|
|
* The handler is stored in a ref so that:
|
|
* 1. The subscription is stable — re-renders don't cause unsubscribe/resubscribe churn.
|
|
* 2. The handler always sees the latest closure values (no stale closures).
|
|
* 3. The parent component's render cycle is never triggered by the subscription itself.
|
|
*
|
|
* @param type - The event name from `AppEvents`.
|
|
* @param handler - Callback receiving the typed payload. May be updated on re-render.
|
|
*
|
|
* @example
|
|
* ```tsx
|
|
* useAppEvent('AUTH:PROFILE_UPDATED', (profile) => {
|
|
* console.log(profile.name); // fully typed, auto-cleaned on unmount
|
|
* });
|
|
* ```
|
|
*/
|
|
export function useAppEvent<K extends keyof AppEvents>(
|
|
type: K,
|
|
handler: (event: AppEvents[K]) => void,
|
|
): void {
|
|
// Always keep the latest handler in a ref to avoid stale closures
|
|
// and prevent re-subscription on every render.
|
|
const handlerRef = useRef(handler);
|
|
handlerRef.current = handler;
|
|
|
|
useEffect(() => {
|
|
// Create a stable delegate that forwards to the latest handler ref
|
|
const delegate = (event: AppEvents[K]) => {
|
|
handlerRef.current(event);
|
|
};
|
|
|
|
eventBus.on(type, delegate);
|
|
|
|
// Cleanup: unsubscribe when the component unmounts or `type` changes.
|
|
// This is the critical memory-leak prevention mechanism.
|
|
return () => {
|
|
eventBus.off(type, delegate);
|
|
};
|
|
}, [type]);
|
|
}
|
|
|
|
// ─── usePublishEvent ────────────────────────────────────────────
|
|
|
|
/**
|
|
* Returns a strictly typed `publish` function.
|
|
*
|
|
* The returned function is referentially stable (memoized) so it
|
|
* can be safely passed as a prop or used in dependency arrays
|
|
* without causing unnecessary re-renders.
|
|
*
|
|
* @example
|
|
* ```tsx
|
|
* const publish = usePublishEvent();
|
|
*
|
|
* const handleClick = () => {
|
|
* publish('DEVICE:PRINT_RECEIPT', {
|
|
* receiptId: '001',
|
|
* items: [{ name: 'Widget', qty: 2, price: 9.99 }],
|
|
* total: 19.98,
|
|
* cashierName: 'Firman',
|
|
* timestamp: Date.now(),
|
|
* });
|
|
* };
|
|
* ```
|
|
*/
|
|
export function usePublishEvent(): typeof busPublish {
|
|
return useCallback(busPublish, []);
|
|
}
|