Thin and simple functional event system with strong typing.
Signal size is with no external dependencies.
Inspired from Robert Penner's AS3 Signals.
Source code in Typescript, compiled to ESM & CJS Javascript thanks to TSBundle.
Works in Node and Browser environments.
Signal ➡ Concept / Usage / Naming Signals / Remove / State Signal / Unpkg
Classic event dispatcher systems are string based, which can be difficult to track across your application.
document.addEventListener( "which one already ?", () => {} );
With Signal, every event is represented by an entity with add
, remove
and dispatch
methods.
Messages can be dispatched and followed more fluently thanks to its dot notation.
const onMessage = Signal()
onMessage.add( message => {
console.log( message ) // { from: "Michael", content: "Hello !" }
})
onMessage.dispatch({
from: "Michael",
content: "Hello !"
})
Signal follow the composition over inheritance concept of design patterns to allow highly scalable projects and libraries. Ne need to extend EventDispatcher again. Simple example of composition with several Signals :
function createMessageSystem () { // No class, no inheritence, no pain
return {
// Two events -> two entities, no string used here
onConnected: Signal<[ Boolean ]>(), // Optional, can pass type of arguments
onMessage: Signal(), // No type here, so no check of passed object with TS
connect () {
// ...
onConnected.dispatch( true );
},
sendMessage ( userName:string, content:string ) {
// ...
onMessage.dispatch( { from: userName, content } )
}
}
}
const messageSystem = createMessageSystem();
messageSystem.onConnected.once( state => {
// Called once when connected
})
messageSystem.connect();
messageSystem.onMessage.add( message => {
console.log( message )
})
messageSystem.sendMessage("Bernie", "Hey")
// ...
messageSystem.sendMessage("Bernie", "What'up ?")
Signal are object entities which can and should be named correctly. It's better to name signal prefixed with "on" and with usage of preterit if possible.
✅ onMessage
✅ onMessageReceived
🚫 message
🚫 messageReceived
🚫 receiveMessage
---
✅ onConnected
✅ onData
✅ onDataSent
✅ onDataReceived
Signal handlers can be detached with the remove function, but you need to keep track of the handler's reference.
function handler () {
// Called once
}
onSignal.add( handler )
onSignal.dispatch()
// ...
onSignal.remove( handler ) // dettach listener
onSignal.dispatch()
For convenience and easier usage, when a signal is attached, a remove thunk is returned. It allows fast removal of anonymous handlers without having to target it manually.
const removeListener = onSignal.add(() => {
// Called once
})
onSignal.dispatch()
// ...
removeListener() // dettach listener without handler ref
onSignal.dispatch()
Works well with React Hooks :
function ReactComponent ( props ) {
useLayoutEffect(() => {
// onData.add returns the remove function,
// so the layoutEffect will remove when component will be destroyed
return Model.onData.add( data => {
})
}, [])
return <div></div>
}
Can be shortened to
function ReactComponent ( props ) {
useLayoutEffect(() => Model.onData.add( data => {
// Data changed, listener will be removed automatically with component
}))
}
To clear all listeners. Useful to dispose a signal and allow garbage collection.
onSignal.clear();
StateSignal is a kind of Signal which holds the last dispatched value. A StateSignal can be initialized with a default value.
// No need for generics here, state type is gathered from default value
const onStateSignal = StateSignal( 12 ) // 12 is the default value here
console.log(onStateSignal.state) // == 12
onStateSignal.add( value => {
// Is dispatched twice.
console.log( value )
// 1st -> 12 (call at init)
// 2nd -> 15 (dispatch)
}, true) // True here means "call at init" (will call handler when attached)
// Read and alter state
if ( onStateSignal.state === 12 )
onStateSignal.dispatch( 15 ) // Change the state value
State Signal will send old value as second argument. It can be useful to diff changes.
const onStateSignal = StateSignal( 12 )
onStateSignal.add( ( newValue, oldValue ) => {
// Continue only when value actually changes
if ( newValue == oldValue )
return
if ( newValue > oldValue )
console.log("Greater")
else
console.log("Smaller")
})
onStateSignal.dispatch( 15 ) // Greater
onStateSignal.dispatch( 5 ) // Smaller
onStateSignal.dispatch( 5 ) // No effect
Signal is available on unpkg CDN as :