Skip to content

zouloux/signal

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

28 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Signal

Thin and simple functional event system with strong typing. Signal size is less than 300b 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.


SignalConcept /  Usage /  Naming Signals /  Remove /  State Signal /  Unpkg


Concept

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 !"
})

Usage

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 ?")

Naming Signals

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

Remove

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();

State Signal

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

Unpkg

Signal is available on unpkg CDN as :

About

Thin and simple entity-based event system

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published