A type-safe, channel-based event dispatcher system for Nuxt with automatic cleanup and flexible event handling.
- Simple event dispatcher system wrapping
mitt - Create event buses with strong types quickly
- Create custom event types
- TypeScript-safe and type-safe
bun add @type32/dispatcherCreate a typed event schema using the DispatcherEvent wrapper:
// types/events.ts
import type { DispatcherEvent } from '@type32/dispatcher'
export interface AppEvents {
user: {
login: DispatcherEvent<{ username: string; id: number }>
logout: DispatcherEvent // No payload
}
notification: {
show: DispatcherEvent<{ message: string; type: 'success' | 'error' | 'info' }>
hide: DispatcherEvent
}
modal: {
open: DispatcherEvent<{ modalId: string; props?: Record<string, any> }>
close: DispatcherEvent<{ modalId: string }>
}
}The useEventDispatcher composable is auto-imported in Nuxt:
<script setup>
import type { AppEvents } from '~/types/events'
// Create a dispatcher with a channel key (recommended)
const events = useEventDispatcher<AppEvents>('app')
// Listen to events (auto-cleanup on unmount)
events.on('user.login', (data) => {
console.log('User logged in:', data. username, data.id)
})
events.on('notification.show', (data) => {
// Show notification UI
console.log(data.message, data.type)
})
// Emit events
const handleLogin = () => {
events.emit('user.login', { username: 'john', id: 123 })
}
const handleLogout = () => {
events.emit('user.logout') // No payload required
}
</script>Channel keys are highly recommended, especially when using the dispatcher within composables.
// composables/useNotifications.ts
export function useNotifications() {
// Same channel key = same event bus across all usages
const events = useEventDispatcher<AppEvents>('app')
const showNotification = (message: string, type: 'success' | 'error') => {
events.emit('notification.show', { message, type })
}
return { showNotification }
}
// pages/index.vue - Will receive events
const events = useEventDispatcher<AppEvents>('app')
events.on('notification.show', (data) => {
// This works! Same channel key = shared bus
})
// pages/other.vue - Trigger from anywhere
const { showNotification } = useNotifications()
showNotification('Hello!', 'success')// composables/useNotifications. ts
export function useNotifications() {
// ⚠️ Each call creates a NEW isolated bus!
const events = useEventDispatcher<AppEvents>()
const showNotification = (message: string) => {
events.emit('notification.show', { message, type: 'info' })
}
return { showNotification }
}
// pages/index.vue
const events = useEventDispatcher<AppEvents>() // Different bus!
events.on('notification.show', (data) => {
// ❌ Won't receive events from useNotifications()
})When to omit channel keys:
- Component-local events that don't need to be shared
- Temporary event buses for isolated features
- Testing and prototyping on one single file
Creates or retrieves an event dispatcher for the specified channel.
Parameters:
channelKey(optional): String identifier for the channel. Omit to create an isolated instance.
Returns: Event dispatcher instance with the following methods:
Emit a typed event with an optional payload.
events.emit('user.login', { username: 'john', id: 123 })
events.emit('user.logout') // No payloadListen to an event. Automatically cleaned up on component unmount.
events.on('user.login', (data) => {
console.log(data.username, data.id)
})Manually remove an event listener.
const handler = (data) => console.log(data)
events.on('user.login', handler)
events.off('user.login', handler)Listen to an event once, then automatically remove the listener.
events.once('modal.close', (data) => {
console.log('Modal closed:', data.modalId)
})Fire-and-forget untyped event (no type checking, no history).
events.yeet('debug.log', { message: 'Something happened' })
events.yeet('analytics.track', 'button-clicked')Listen to untyped events.
events.catch('debug.log', (data) => {
console.log('Debug:', data)
})Remove an untyped event listener.
Remove all listeners from this dispatcher instance.
events.clear()IntelliSense autocompletes event paths and validates payload types:
events.emit('user.login', { username: 'john', id: 123 }) // ✅ Valid
events.emit('user.login', { wrong: 'data' }) // ❌ TypeScript error
events.emit('user.logout', { extra: 'data' }) // ❌ TypeScript errorAll event listeners are automatically removed when the component unmounts (using onUnmounted provided by vue) — no manual cleanup needed!
Organize events hierarchically:
events.on('window.file.newFile', handler)
events.on('player.movement.walk', handler)
events.on('ui.modal.open', handler)Multiple channels for different contexts:
const uiEvents = useEventDispatcher<UIEvents>('ui')
const gameEvents = useEventDispatcher<GameEvents>('game')
// Events don't cross channels
uiEvents.emit('modal.open', { modalId: 'settings' })
// gameEvents won't receive thisFor debug logs, analytics, or temporary events without type constraints:
Note: Wild events called using yeet() can only be caught by using the catch() function. The wild event bus is separate from the normal event bus.
events.yeet('temp.debug', { whatever: 'data' })
events.catch('temp.debug', console.log)// composables/useGlobalEvents.ts
import type { GlobalEvents } from '~/types/events'
export const useGlobalEvents = () => useEventDispatcher<GlobalEvents>('global')export const useUIEvents = () => useEventDispatcher<UIEvents>('ui')
export const useGameEvents = () => useEventDispatcher<GameEvents>('game')
export const usePlayerEvents = () => useEventDispatcher<PlayerEvents>('player')<!-- components/LoginForm.vue -->
<script setup>
const events = useEventDispatcher<AppEvents>('app')
const handleSubmit = async (credentials) => {
const user = await login(credentials)
events.emit('user.login', { username: user.name, id: user.id })
}
</script>
<!-- components/UserMenu.vue -->
<script setup>
const events = useEventDispatcher<AppEvents>('app')
const isLoggedIn = ref(false)
events.on('user.login', (data) => {
isLoggedIn.value = true
console.log('Welcome', data.username)
})
events.on('user.logout', () => {
isLoggedIn.value = false
})
</script>Local development
# Install dependencies
bun i
# Generate type stubs
bun run dev:prepare
# Develop with the playground
bun run dev
# Build the playground
bun run dev:build
# Run ESLint
bun run lint
# Run Vitest
bun run test
bun run test:watch
# Release new version
bun run release