Skip to content

A reactive programming library for JavaScript.

License

Notifications You must be signed in to change notification settings

nullobject/bulb

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Bulb

Build Status

Bulb is a reactive programming library for JavaScript. It provides a simple API for writing event-based programs in a declarative style.

The main data structure introduced by Bulb is called a signal. A signal represents a time-varying source of values — for example, the value of a text input, a periodic timer, or the position of the mouse pointer in the browser window.

The Bulb API provides many functions for creating signals from various sources (e.g. arrays, timers, AJAX requests, DOM events, etc.) and for modifying signals using combinators.

A number of libraries already exist for reactive programming in JavaScript (e.g. RxJS, Bacon.js, Most.js), but Bulb differs in that it tries to avoid providing a "kitchen sink". Instead, Bulb defines a very focussed API that contains only the key building blocks for reactive programming in JavaScript.

Features:

Table of Contents

Installation

Node

Install the npm package:

> npm install bulb

Require it in your code:

import { Signal } from 'bulb'

Browser

The easiest way to start using Bulb in your browser is to include it with a <script> tag in your HTML file:

<script src="https://unpkg.com/bulb/dist/bulb.min.js"></script>

Documentation

What is a Signal? 🚥

The term signal is borrowed from hardware description languages, which allow electrical signals to be modelled as they travel through circuits. Much like in circuits, signals represent a time-varying flow of data – they can be split apart, joined together, and modified as they travel through a system.

Signals are:

  • Directed: Data travels through a network of signals in only one direction.
  • Composable: Signals can be composed together to create new signals.
  • Lazy: Signals don't do anything until they actually need to (i.e. an observer has subscribed).

Signals at a Glance 👀

Let's create a simple signal that emits some values and logs them to the console:

import { Signal } from 'bulb'

const s = Signal.of(1, 2, 3)

s.subscribe({
  next (a) { console.log(a) }
})

Here, Signal.of(1, 2, 3) creates a new signal which emits some values in order. At this point, the signal won't actually do anything until an observer subscribes to the signal.

The subscribe method subscribes an observer to the signal. This means that the next callback will be called when the signal emits a value. In this case, it just prints the emitted values to the console.

An observer can also specify other callback types:

  • next: Called when the signal emits a value.
  • error: Called when the signal emits an error.
  • complete: Called when the signal has finished emitting events.

There is also a handy shortcut if you only want to know when the signal emits a value. In this case, you can just pass a single callback instead of an observer object:

s.subscribe(a => console.log(a))

Combinators 💞

Let's continue with our signal from the previous example, but use the map combinator to modify the values before they are logged to the console:

import { Signal } from 'bulb'

const s = Signal.of(1, 2, 3)
const t = s.map(a => a + 1)

t.subscribe(console.log) // 2, 3, 4

In this example, we created a completely new signal t, by mapping a function over the original signal s. When we subscribe to the new signal t, the modified values are printed in the console.

Another useful combinator is scan:

import { Signal } from 'bulb'

const s = Signal.of(1, 2, 3)
const t = s.scan((a, b) => a + b, 0)

t.subscribe(console.log) // 0, 1, 3, 6

In this example we created a signal t, that takes the values emitted by the signal s and emits the running total of the values, starting from zero. The function (a, b) => a + b is called for every value emitted by the signal s, where a is the accumulated value, and b is the emitted value. Note that the scan combinator will emit the accumulated value for every value emitted by the signal s.

Some combinators wait until the signal has completed before they emit a value. The fold combinator is similar to the scan combinator, but it differs in that it doesn't emit intermediate values. The final value will only be emitted after the signal has completed:

import { Signal } from 'bulb'

const s = Signal.of(1, 2, 3)
const t = s.fold((a, b) => a + b, 0)

t.subscribe(console.log) // 6

In this example, we created a signal t, that takes the values emitted by the signal s and calculates the total of the emitted values, starting from zero. The function (a, b) => a + b is called for every value emitted by the signal s, where a is the accumulated value, and b is the emitted value. Note that the scan combinator will only emit the accumulated value once the signal s has completed.

The Signal Life Cycle ♻️

As we saw previously, to subscribe an observer to a signal we use the subscribe method. This method returns a subscription handle, which we can use to unsubscribe from the signal at a later point in time.

This can be useful for dealing with infinite signals (infinite signals are signals which never complete, they just keep emitting values forever):

import { Signal } from 'bulb'

const s = Signal.periodic(1000)
const subscription = s.subscribe(console.log) // 0, 1, 2, ...

// Some time later...
subscription.unsubscribe()

In this example, we called the periodic method to create a signal s that emits an increasing number every second. When we subscribe to the signal, we keep a reference to the returned subscription handle. To stop receiving values from the signal, we call the unsubscribe method on the handle.

Get on the Bus 🚌

A Bus is a special type of signal that can be connected with other signals:

import { Bus, Signal } from 'bulb'

const s = Signal.of(1, 2, 3)
const bus = new Bus()
const subscription = bus.subscribe(console.log)

bus.connect(s)

In this example, we created a bus and a signal. We connected the signal s to the bus by calling the connect method, which means that any values emitted by the signal s will be re-emitted by the bus.

Sometimes it is useful to manually emit values on a bus:

import { Bus } from 'bulb'

const bus = new Bus()
const subscription = bus.subscribe(console.log)

bus.next(1)
bus.next(2)
bus.next(3)

In this example, we created a bus and subscribed it to the console logger. We then manually emitted some values by calling the next method on the bus.

Examples

Take a look at some examples of how to use Bulb in the real world:

Licence

Bulb is licensed under the MIT licence. See the LICENCE file for more details.