https://github.com/zouloux/signal
Thin and simple entity-based event system
https://github.com/zouloux/signal
Last synced: 8 months ago
JSON representation
Thin and simple entity-based event system
- Host: GitHub
- URL: https://github.com/zouloux/signal
- Owner: zouloux
- License: mit
- Created: 2022-06-17T08:01:32.000Z (about 4 years ago)
- Default Branch: main
- Last Pushed: 2023-07-30T05:46:25.000Z (almost 3 years ago)
- Last Synced: 2025-04-12T11:07:49.817Z (about 1 year ago)
- Language: TypeScript
- Size: 248 KB
- Stars: 5
- Watchers: 1
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# Signal
[//]: # (TODO : Image)
Thin and simple functional event system with strong typing.
Signal size is ____ with __no external dependencies__.
Inspired from [Robert Penner](https://github.com/robertpenner)'s AS3 Signals.
Source code in Typescript, compiled to ESM & CJS Javascript thanks to [TSBundle](https://github.com/zouloux/tsbundle).
Works in Node and Browser environments.
---
Signal ➡
Concept /
Usage /
Naming Signals /
Remove /
State Signal /
Unpkg
---
### Concept
Classic event dispatcher systems are __string based__, which can be difficult to track across your application.
```typescript
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.
```typescript
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__](https://en.wikipedia.org/wiki/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 :
```typescript
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.
```tsx
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.
```tsx
const removeListener = onSignal.add(() => {
// Called once
})
onSignal.dispatch()
// ...
removeListener() // dettach listener without handler ref
onSignal.dispatch()
```
Works well with React Hooks :
```tsx
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
}
```
Can be shortened to
```tsx
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.
```tsx
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.
```tsx
// 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.
```tsx
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](https://unpkg.com/) CDN as :
- [Only Signal](https://unpkg.com/@zouloux/signal@latest/dist/signal.es2017.min.js) 
- [State-Signal + Signal.js](https://unpkg.com/@zouloux/signal@latest/dist/state-signal.es2017.min) 