Skip to content

km-kwon/broadcast-event-system

Repository files navigation

broadcast-event-system

npm version License: MIT CI codecov

Type-safe event bus and broadcast service for React applications. Pure TypeScript implementation with zero dependencies (except React for hooks).

Features

  • 🎯 Type-safe: Full TypeScript support with generics
  • 🪶 Lightweight: No dependencies (React as peer dependency for hooks)
  • 🔄 Event Bus: In-app event communication
  • 📡 Broadcast: Cross-window/tab communication
  • ⚛️ React Hooks: Easy integration with React components
  • 🧹 Auto Cleanup: Automatic event listener cleanup on unmount

Installation

npm install broadcast-event-system
# or
yarn add broadcast-event-system
# or
pnpm add broadcast-event-system

Quick Start

Event Bus (In-App Communication)

import { useEventEmit, useEventOn } from 'broadcast-event-system';

// Define your event type
type CounterEvent = { count: number };

// Component that emits events
function CounterButton() {
  const emit = useEventEmit();

  const handleClick = () => {
    emit<CounterEvent>('counter:increment', { count: 1 });
  };

  return <button onClick={handleClick}>Increment</button>;
}

// Component that listens to events
function CounterDisplay() {
  useEventOn<CounterEvent>('counter:increment', (data) => {
    console.log('Counter incremented:', data.count);
  });

  return <div>Listening to counter events</div>;
}

Broadcast (Cross-Window Communication)

import { useBroadcast, useBroadcastOn } from 'broadcast-event-system';

// Send messages to other windows/tabs
function Sender() {
  const broadcast = useBroadcast();

  const handleClick = () => {
    broadcast('my-channel', { message: 'Hello from another tab!' });
  };

  return <button onClick={handleClick}>Send to Other Tabs</button>;
}

// Receive messages from other windows/tabs
function Receiver() {
  useBroadcastOn('my-channel', (data) => {
    console.log('Received:', data);
  });

  return <div>Listening to other tabs</div>;
}

API Reference

React Hooks

useEventEmit(service?)

Returns a function to emit events.

const emit = useEventEmit();
emit<MyEventType>('event-name', { data: 'value' });

useEventOn<T>(eventType, callback, service?)

Subscribe to an event. Automatically unsubscribes on unmount.

useEventOn<MyEventType>('event-name', (data) => {
  console.log('Received:', data);
});

useEventState<T>(eventType, initialState, service?)

Combines event subscription with state management.

type UserEvent = { name: string; age: number };

function UserProfile() {
  const user = useEventState<UserEvent>('user:updated', { name: '', age: 0 });

  return <div>{user.name} is {user.age} years old</div>;
}

useBroadcast(service?)

Returns a function to broadcast messages to other windows/tabs.

const broadcast = useBroadcast();
broadcast('channel-name', { data: 'value' });

useBroadcastOn(channelName, callback, service?)

Subscribe to broadcast messages from other windows/tabs.

useBroadcastOn('channel-name', (data) => {
  console.log('Received from another tab:', data);
});

Core Services

EventService

import { EventService, eventService } from 'broadcast-event-system/core';

// Use singleton instance
eventService.emit('event-name', { data: 'value' });
eventService.on('event-name', (payload) => console.log(payload));
eventService.off('event-name', listener);
eventService.clear('event-name');
eventService.clearAll();

// Or create your own instance
const myEvents = new EventService();

BroadcastService

import { BroadcastService, broadcastService } from 'broadcast-event-system/core';

// Use singleton instance
const listenerId = broadcastService.subscribe('channel', (data) => {
  console.log('Received:', data);
});

broadcastService.broadcast('channel', { message: 'Hello' });

// Cleanup - unsubscribe specific listener
broadcastService.unsubscribe('channel', listenerId);

// Or close entire channel (removes all listeners)
broadcastService.close('channel');

// Close all channels
broadcastService.closeAll();

// Or create your own instance
const myBroadcast = new BroadcastService();

Advanced Usage

Custom Event Service Instance

import { EventService } from 'broadcast-event-system/core';
import { useEventEmit, useEventOn } from 'broadcast-event-system';

// Create a custom instance for isolated event bus
const customEvents = new EventService();

function MyComponent() {
  const emit = useEventEmit(customEvents);

  useEventOn('my-event', (data) => {
    console.log(data);
  }, customEvents);

  // This event is isolated from the global event bus
  return <button onClick={() => emit('my-event', { test: true })}>Click</button>;
}

Event Naming Conventions

We recommend using namespaced event names:

// Good
'user:login'
'user:logout'
'cart:add-item'
'cart:remove-item'
'notification:show'

// Avoid
'login'
'addItem'
'show'

TypeScript Best Practices

Define event types for better type safety:

// events.types.ts
export type UserLoginEvent = {
  userId: string;
  timestamp: number;
};

export type CartAddItemEvent = {
  itemId: string;
  quantity: number;
};

// In your component
import { useEventEmit } from 'broadcast-event-system';
import type { UserLoginEvent } from './events.types';

const emit = useEventEmit();
emit<UserLoginEvent>('user:login', {
  userId: '123',
  timestamp: Date.now(),
});

Real-World Example

// EventTypes.ts
export type NotificationEvent = {
  type: 'success' | 'error' | 'info';
  message: string;
  duration?: number;
};

// NotificationManager.tsx
import { useEventOn } from 'broadcast-event-system';
import { NotificationEvent } from './EventTypes';

export function NotificationManager() {
  const [notifications, setNotifications] = useState<NotificationEvent[]>([]);

  useEventOn<NotificationEvent>('notification:show', (data) => {
    setNotifications((prev) => [...prev, data]);

    setTimeout(() => {
      setNotifications((prev) => prev.filter((n) => n !== data));
    }, data.duration || 3000);
  });

  return (
    <div className="notifications">
      {notifications.map((notif, idx) => (
        <div key={idx} className={`notification ${notif.type}`}>
          {notif.message}
        </div>
      ))}
    </div>
  );
}

// Anywhere in your app
import { useEventEmit } from 'broadcast-event-system';
import { NotificationEvent } from './EventTypes';

function MyForm() {
  const emit = useEventEmit();

  const handleSubmit = async () => {
    try {
      await saveData();
      emit<NotificationEvent>('notification:show', {
        type: 'success',
        message: 'Data saved successfully!',
      });
    } catch (error) {
      emit<NotificationEvent>('notification:show', {
        type: 'error',
        message: 'Failed to save data',
      });
    }
  };

  return <button onClick={handleSubmit}>Save</button>;
}

Testing

This package includes comprehensive test coverage using Vitest and React Testing Library.

Running Tests

# Run all tests
npm test

# Run tests in watch mode
npm run test:watch

# Run tests with coverage
npm run test:coverage

# Open Vitest UI
npm run test:ui

Test Coverage

The project maintains high test coverage:

  • EventService: 100% coverage
  • BroadcastService: 100% coverage
  • React Hooks: 100% coverage
    • useEventEmit
    • useEventOn
    • useEventState
    • useBroadcast
    • useBroadcastOn

Total: 122 tests across 6 test suites

Coverage reports are generated in the coverage/ directory. Open coverage/index.html in your browser to view detailed coverage information.

CI/CD

This project uses GitHub Actions for continuous integration:

  • Automated Testing: Tests run on Node.js 18, 20, and 22
  • Build Verification: Ensures TypeScript compilation succeeds
  • Coverage Reporting: Automatic upload to Codecov

See .github/workflows/ci.yml for the complete CI configuration.

Changelog

See CHANGELOG.md for version history and release notes.

License

MIT

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors