suber
v5.0.1
Published
A message bus / pubsub compatible with Redux middlewares
Downloads
1,057
Maintainers
Readme
Why Suber?
- Helpful: Decouples your application modules / components which improves testability
- Tiny: weighs ≈ 1kb gzipped
- Compatible: Works with popular side effects handling middlewares like redux-saga and redux-observable
- Extensible: Easily create middleware to extend it
- No dependencies: No dependencies, it's less than 100 lines of code
- Framework bindings: See react-suber for React binding and preact-suber for Preact binding
Usage
Install:
yarn add suber
# or
npm install suber --save
Regular usage:
// Import
import { createBus } from 'suber'
const bus = createBus()
// Listen for a message until manual unsubscription
const unsub = bus.take('MY_CHANNEL', (msg) => {
console.log(msg.title)
})
// or just one (unsubscribes automatically)
bus.one('MY_CHANNEL', (msg) => {
console.log(msg.title)
})
// Send a message / data
bus.send('MY_CHANNEL', { title: 'Hello World' })
Usage in combination with Redux:
// app.js
// App init file
import { createBus, createReduxMiddleware } from 'suber'
import { createStore, applyMiddleware } from 'redux'
// Init
const bus = createBus()
const mw = createReduxMiddleware(bus)
// Next line enables sending actions from Redux into suber
const store = createStore(
yourApp,
applyMiddleware(mw) // combine with your other middlewares
)
// Send everything from suber into Redux
bus.applyMiddleware((_) => (channel, message, source) => {
// No loop-backs
if (source === 'redux') return
// Send to Redux with the channel as the action type
store.dispatch({...message, type: channel})
})
// Imagine you have redux-saga setup listening for actions
// with the type: 'GET_USER' and dispatches a new action with
// type 'GOT_USER_RESPONSE_' + id, when response arrives from API.
// ----
// component.js
// React component or anything that has been given access to the suber bus
// either via suber bindings like 'react-suber' or 'preact-suber'
// or through a jsx property.
// Listen for a single message on that channel
bus.one('GOT_USER_RESPONSE_1', (data) => {
console.log(data)
})
// Send message to redux-saga to fetch user with id = 1
bus.send('GET_USER', { id: 1 })
Use cases
Regular
It can be used as the main eventbus / pubsub in an application to keep components separated without any dependencies (except for suber) and be able to communicate.
For code testability it's better to have all side effect handlers listening and acting on messages on a bus rather than to have async / ajax calls directly in the components. With subers Redux middleware compability, great middlewares like redux-observable and redux-saga can be used to handle the side effects.
On the other hand, creating a suber middleware is super easy so having direct fetch calls in a self-made middleware is often enough for smaller applications.
In combination with Redux
Redux is great for shared application state handling, but often too much state are stored in Redux.
Application state that is not shared should not go into Redux, it should go directly, and only into, the component that needs it.
This is where suber comes in.
Setup your existing Redux middleware side-effect handler (like redux-saga, thunks or redux-observable)
to listen for certain action types (GET_USER
in the below example) and pass suber to
your component (or have a container component handle the calls and pass the response as props to it).
Your component can now super easy with
bus.one('GOT_USER_1', (r) => this.setState({user: r.user}))
bus.send('GET_USER', {id: 1})
do API calls and set the component state so component can update the DOM can accordingly. Now all code with side effects is in the same place no matter if the endpoint for the response is Redux or single component state.
API
Factory
Methods
Utility functions
Factory
Factories are functions that returns you the bus.
createBus()
Creates and returns a new bus instance. This is NOT a global singleton anymore.
Methods
These are methods that are attached to the bus instance.
take(channel, fn, filterFn)
Sets up a listener on the bus and calls fn
every time a message with a matching channel
arrives.
Arguments
channel: String
What channel to listen on the bus.fn: Function(message)
The function to call when a message on the channel arrives.filterFn: Function(message)
An optional filter function that can be used to just listen for a specific kind of data on the channel. See test file for example.
Returns unsubscribe: Function
Call this function to unsubscribe this fn
on the channel.
one(channel, fn, filterFn)
Sets up a listener on the bus and calls fn
one time time once a message with a matching channel
arrives.
Just like take
above but with automatic unsubscription after the first message.
send(channel, message, source)
Send a message on a channel on the bus.
Arguments
channel: String
The channel to send the message on.message: any
The message, can be of any data type.source: String
Optional argument to specify the message source. Best used whenapplyMiddleware
andcreateReduxMiddleware
both are specified to avoid loop-backs.
Returns void
self(channel, message, fn)
Send a message on a channel and expect the receiver of the message to reply back to you.
A property (named $$responseChannel
) is automatically added to the message for the
subscriber to respond on. See tests file for an example.
Arguments
channel: String
The channel to send the message on.message: any
The message, can be of any data type.fn: Function
Function to be called with the response from the subscriber.
Returns void
reset()
Removes all subscribers on all channels.
Arguments
No arguments
Returns void
applyMiddleware(fn)
Add middleware to Suber. All messages on all channels gets passed to the middleware.
Function1 are called when applying the middleware anf gives access to the send
method the the bus. The originObject
should only be used when a middleware is used to send all message into Redux, and a Redux middleware created with createReduxMiddleware
in combination. This stops actions from being sent in an infinite loop. See test files for example usages.
Function2 are called on every message passing through the bus.
Arguments
fn: Function1(send, originObject) => Function2(channel, message, source)
The function to be called with every message on the bus.
Returns void
resetMiddlewares()
Removes all active all middlewares.
Arguments
No arguments
Returns void
applyReduxMiddleware(rmw)
Add a Redux middleware to Suber. This way the excellent Redux middlewares for
handling side effects like redux-saga and redux-observable can be used without
Redux.
The params for store.getState
and next
are unsuable, so middlewares that rely on
these will not be compatible since Suber isn't about state / reducers.
Arguments
rmw: Function()
The Redux middleware with(store) => (next) => (action) =>
signature.
Returns void
Utility functions
Functions to configure or extend the bus.
createReduxMiddleware(bus)
Creates a function that has the redux middleware signature. This function repeats everything on the Redux bus into the passed in Suber bus. Redux action types are mapped to be suber channels.
redux.action = {
type: 'MY_ACTION',
some: 'data',
more: 'data'
}
// becomes
suber.send(redux.action.type, redux.action, 'redux')
Arguments
bus: Object
A Suber bus instance gotten fromcreateBus()
.
Returns Function
to be passed into Redux's applyMiddleware
Development setup
yarn
npm run dev