ts-bus
A lightweight TypeScript event bus to help manage your application architecture
Example
; // Define Event; // Create bus; // Subscribebus.subscribesomeEvent, ; // Publishbus.publishsomeEvent;
Rationale
We want to write loosely coupled highly cohesive applications and one of the best and easiest ways to do that is to use an event bus as a management layer for our applications.
This is the kind of thing that you could use effectively in most applications.
For my purposes I wanted a system that:
- Is framework agnostic can support Vue, React or Angular.
- Could enable micro-frontends / microlithic architecture.
- Can easily use React hooks to reduce state in the case of React.
- Does not conflate eventing with state management.
- Has really good TypeScript support.
Alternatives
- Redux - conflates state management with eventing and causes complexity around async as a result. Redux has a highly invasive syntax that is difficult to remove or abstract out of an application. React comes with state management out of the box these days anyway. See my article "Life after Redux"
- RxJS - could make a great event bus but feels too heavy handed for use with many projects.
- Node
events
- is a little too much API for what I need here. This lib actually decorates theEventEmitter2
package. In the future I may remove it to become dependency free.
Installation
Use your favourite npm client to install ts-bus. Types are included automatically.
Npm:
npm install ts-bus
Yarn:
yarn add ts-bus
Example applications
Usage
Create a bus
Create your EventBus globally somewhere:
// bus.ts;;
Declare events
Next create some Events:
// events.ts; ; ;
Notice createEventDefinition()
will often be called with out a runtime check argument and it returns a function that accepts the event type as an argument. Whilst possibly a tiny bit awkward, this is done because it is the only way we can allow effective discriminated unions. See switching on events.
Runtime payload checking
You can also provide a predicate to do runtime payload type checking in development. This is useful as a sanity check if you are working in JavaScript:
; // pdsl creates predicate functionsconst isLabel = p`{ id: string, label: string,}`; const taskLabelUpdated = "task.label.updated"; ; // {"id":"abc"} does not match expected payload.
These warnings are suppressed in production.
Subscribing
;; // You can subscribe using the event creator functionbus.subscribetaskLabelUpdated, ;
Unsubscribing
To unsubscribe from an event use the returned unsubscribe function.
; unsubscribe; // removes event subscription
Subscribing with a type string
You can use the event type to subscribe.
bus.subscribe"task.created", ;
Or you can use wildcards:
bus.subscribe"task.**", ;
Subscribing with a predicate function
You can also subscribe using a predicate function to filter events.
// A predicate bus.subscribeisSpecialEvent, ;
You may find pdsl a good fit for creating predicates.
Subscription syntax
As you can see above you can subscribe to events by using the subscribe
method of the bus.
;
This subscription function can accept a few different options for the first argument:
- A
string
that is the specific event type or a wildcard selector eg.mything.**
. - An
eventCreator
function returned fromcreateEventDefinition<PayloadType>()("myEvent")
- A
predicate
function that will only subscribe to events that match the predicate. Note the predicate function matches the entireevent
object not just the payload. Eg.{type:'foo', payload:'foo'}
The returned unsubscribe()
method will unsubscribe the specific event from the bus.
Publishing events
Now let's publish our events somewhere
// publisher.ts;;
Using a plain event object
If you want to avoid the direct dependency with your event creator you can use the plain event object:
bus.publish;
Republishing events
Lets say you have received a remote event from a websocket and you need to prevent it from being automatically redispatched you can provide custom metadata with each publication of an event to prevent re-emmission of events over the socket.
; // get an event from a socketsocket.on"event-sync",; // This is a shorthand utility that creates predicate functions to match based on a given object shape.// For more details see https://github.com/ryardley/pdsl; // Prevent sending a event-sync if the event was remotebus.subscribeisSharedAndNotRemoteFn, ;
Switching on Events and Discriminated Unions
// This function creates foo events; // This function creates bar events; // Create a union type to represent your app events; bus.subscribe"shared.**",;
Wildcard syntax
You can namespace your events using period delimeters. For example:
"foo.*" matches "foo.bar"
"foo.*.thing" matches "foo.fing.thing"
"**" matches everything eg "foo" or "foo.bar.baz"
"*" matches everything within a single namespace eg. "foo" but not "foo.bar"
This is inherited directly from EventEmitter2 which ts-bus currently uses under the hood.
React extensions
Included with ts-bus
are some React hooks and helpers that provide a bus context as well as facilitate state management within React.
BusProvider
Wrap your app using the BusProvider
;; ;; // global bus; // This wraps React Context and passes the bus to the `useBus` hook.;
useBus
Access the bus instance with useBus
// Dispatch from deep in your application somewhere...;;
useBusReducer
This connects state changes to bus events via a state reducer function.
Its signature is similar to useReducer except that it returns the state object instead of an array:
Example:
;
; ;
Custom subscriber function
You can configure useBusReducer
with a custom subscriber
passing in an options object.
// get a new useReducer function; ;
NOTE: Boilerplate can be reduced by using the reducerSubscriber
function.
useBusReducer.configure;
Usage with Redux dev tools
You can use ts-bus with Redux Devtools by using Reinspect.
Here is an example:
;;;; ; ; ;;
useBusReducer configuration
Available options:
Option | Description |
---|---|
subscriber | Reducer subscriber definition |
useReducer | Alternate React.useReducer implementation |
useBusState
This connects state changes to bus events via a useState equivalent function.
; ;
useBusState configuration
You can configure useBusState with a subscriber passing in an options object.
// get a new useState function; ;
NOTE: The boilerplate code can be reduced by using the stateSubscriber function.
;
Available options:
Option | Description |
---|---|
subscriber | State subscriber definition |