Convert Figma logo to code with AI

baconjs logobacon.js

Functional reactive programming library for TypeScript and JavaScript

6,471
331
6,471
87

Top Related Projects

30,609

A reactive programming library for JavaScript

2,375

An extremely intuitive, small, and fast functional reactive stream library for JavaScript

3,495

Ultra-high performance reactive programming

1,873

A Reactive Programming library for JavaScript

Observables for ECMAScript

Quick Overview

Bacon.js is a functional reactive programming (FRP) library for JavaScript. It provides a way to handle asynchronous events and values over time, allowing developers to create complex event-driven applications with ease. Bacon.js implements the Observable pattern and offers a rich set of operators for transforming and combining event streams.

Pros

  • Powerful and expressive API for handling asynchronous operations
  • Helps reduce callback hell and simplifies complex event-driven code
  • Supports both Node.js and browser environments
  • Well-documented with comprehensive examples and tutorials

Cons

  • Steep learning curve for developers new to functional reactive programming
  • Can lead to performance issues if not used carefully with large data streams
  • Less active development and community support compared to some alternatives (e.g., RxJS)
  • May be overkill for simpler applications that don't require complex event handling

Code Examples

Creating an event stream from DOM events:

const clicks = Bacon.fromEvent(document, 'click');
clicks.onValue(event => console.log('Clicked at:', event.clientX, event.clientY));

Combining multiple streams:

const firstName = Bacon.fromPromise(fetchFirstName());
const lastName = Bacon.fromPromise(fetchLastName());

Bacon.combineTemplate({
  first: firstName,
  last: lastName
}).onValue(name => console.log(`Full name: ${name.first} ${name.last}`));

Throttling events:

const mouseMoves = Bacon.fromEvent(document, 'mousemove');
const throttledMoves = mouseMoves.throttle(100);

throttledMoves.onValue(event => updateCursorPosition(event.clientX, event.clientY));

Getting Started

To start using Bacon.js in your project:

  1. Install Bacon.js via npm:

    npm install baconjs
    
  2. Import Bacon.js in your JavaScript file:

    import Bacon from 'baconjs';
    
  3. Create an event stream and start using Bacon.js:

    const button = document.querySelector('#myButton');
    const clicks = Bacon.fromEvent(button, 'click');
    
    clicks.onValue(() => console.log('Button clicked!'));
    

This basic setup allows you to create event streams and react to events using Bacon.js. Explore the documentation for more advanced usage and operators.

Competitor Comparisons

30,609

A reactive programming library for JavaScript

Pros of RxJS

  • Larger community and ecosystem, with more resources and third-party libraries
  • More comprehensive API with a wider range of operators and utilities
  • Better TypeScript support and integration

Cons of RxJS

  • Steeper learning curve due to its more extensive API and concepts
  • Larger bundle size, which may impact performance in smaller projects
  • More complex syntax for some operations compared to Bacon.js

Code Comparison

RxJS:

import { fromEvent } from 'rxjs';
import { map, filter } from 'rxjs/operators';

fromEvent(document, 'click')
  .pipe(
    map(event => event.clientX),
    filter(x => x > 200)
  )
  .subscribe(x => console.log(x));

Bacon.js:

Bacon.fromEvent(document, 'click')
  .map('.clientX')
  .filter(x => x > 200)
  .onValue(x => console.log(x));

Both libraries provide similar functionality for handling reactive programming concepts. RxJS offers a more extensive set of features and better TypeScript support, making it suitable for larger projects. Bacon.js, on the other hand, has a simpler API and may be easier to learn for beginners. The choice between the two depends on project requirements, team expertise, and performance considerations.

2,375

An extremely intuitive, small, and fast functional reactive stream library for JavaScript

Pros of xstream

  • Smaller bundle size and lighter weight than Bacon.js
  • Designed specifically for Cycle.js, offering seamless integration
  • Simpler API with fewer operators, making it easier to learn and use

Cons of xstream

  • Less mature and less widely adopted compared to Bacon.js
  • Fewer built-in operators and combinators, potentially requiring more custom implementations
  • Limited community support and ecosystem compared to Bacon.js

Code Comparison

xstream:

import xs from 'xstream';

const stream = xs.periodic(1000).map(i => i * 2);
stream.addListener({
  next: i => console.log(i),
  error: err => console.error(err),
  complete: () => console.log('completed')
});

Bacon.js:

import Bacon from 'baconjs';

const stream = Bacon.interval(1000).map(i => i * 2);
stream.onValue(i => console.log(i));
stream.onError(err => console.error(err));
stream.onEnd(() => console.log('completed'));

Both libraries provide similar functionality for creating and manipulating streams. xstream uses a more explicit listener object, while Bacon.js offers separate methods for different event types. xstream's API is generally more concise, reflecting its focus on simplicity and lightweight design.

3,495

Ultra-high performance reactive programming

Pros of most

  • Higher performance and lower memory usage
  • More comprehensive and modern API
  • Better support for asynchronous operations

Cons of most

  • Steeper learning curve for beginners
  • Smaller community and ecosystem compared to Bacon.js
  • Less documentation and fewer examples available

Code Comparison

most:

import { from, map, filter } from 'most';

from([1, 2, 3, 4])
  .map(x => x * 2)
  .filter(x => x > 5)
  .observe(x => console.log(x));

Bacon.js:

Bacon.fromArray([1, 2, 3, 4])
  .map(x => x * 2)
  .filter(x => x > 5)
  .onValue(x => console.log(x));

Both libraries provide similar functionality for creating and manipulating event streams. However, most uses a more modern syntax with ES6 imports and promises, while Bacon.js relies on a more traditional approach. most's API is generally more concise and offers better performance for complex operations. Bacon.js, on the other hand, has a larger community and more resources available for learning and troubleshooting.

1,873

A Reactive Programming library for JavaScript

Pros of Kefir

  • Smaller bundle size and better performance
  • More active development and maintenance
  • Better TypeScript support

Cons of Kefir

  • Less comprehensive documentation
  • Smaller community and ecosystem

Code Comparison

Bacon.js:

Bacon.fromEvent(document, "click")
  .map(e => e.clientX)
  .filter(x => x > 100)
  .onValue(x => console.log(x));

Kefir:

Kefir.fromEvents(document, "click")
  .map(e => e.clientX)
  .filter(x => x > 100)
  .onValue(x => console.log(x));

Both Bacon.js and Kefir are Functional Reactive Programming (FRP) libraries for JavaScript. They provide similar functionality for handling asynchronous events and data streams.

Kefir offers better performance and a smaller footprint, making it more suitable for projects where size and speed are crucial. It also has more active development and better TypeScript support, which can be beneficial for modern JavaScript projects.

However, Bacon.js has more comprehensive documentation and a larger community, which can be advantageous for developers seeking resources and support.

The code comparison shows that both libraries have similar syntax for creating and manipulating event streams. The main difference is in the initial method name: Bacon.js uses fromEvent, while Kefir uses fromEvents.

When choosing between the two, consider your project's specific needs, such as performance requirements, TypeScript usage, and the importance of community support.

Observables for ECMAScript

Pros of proposal-observable

  • Standardized approach, potentially becoming part of ECMAScript
  • Simpler API with fewer methods, focusing on core functionality
  • Better integration with existing JavaScript ecosystem

Cons of proposal-observable

  • Less feature-rich compared to Bacon.js
  • Still in proposal stage, not yet finalized or widely implemented
  • May require polyfills or transpilation for current use

Code Comparison

proposal-observable:

const observable = new Observable(observer => {
  observer.next(1);
  observer.next(2);
  observer.complete();
});
observable.subscribe(x => console.log(x));

Bacon.js:

const stream = Bacon.fromArray([1, 2]);
stream.onValue(x => console.log(x));

Key Differences

  • Bacon.js offers more advanced features like combining streams and handling errors
  • proposal-observable aims for a minimal, standardized approach
  • Bacon.js has a more functional programming style
  • proposal-observable follows a more object-oriented pattern

Use Cases

  • Bacon.js: Complex event handling, reactive programming
  • proposal-observable: Simple asynchronous data streams, potential future standard

Community and Support

  • Bacon.js: Established library with active community
  • proposal-observable: Backed by TC39, potential for widespread adoption if standardized

Convert Figma logo designs to code with AI

Visual Copilot

Introducing Visual Copilot: A new AI model to turn Figma designs to high quality code using your components.

Try Visual Copilot

README

Bacon.js

A functional reactive programming lib for TypeScript JavaScript, written in TypeScript.

Turns your event spaghetti into clean and declarative feng shui bacon, by switching from imperative to functional. It's like replacing nested for-loops with functional programming concepts like map and filter. Stop working on individual events and work with event streams instead. Combine your data with merge and combine. Then switch to the heavier weapons and wield flatMap and combineTemplate like a boss.

Here's the stuff.

Build Status BrowserStack Status NPM version Dependency Status devDependency Status

Install and Usage

Typescript

Bacon.js starting from version 3.0 is a Typescript library so you won't need any external types. Just Install using npm.

npm install baconjs

Then you can

import { EventStream, once } from "baconjs"

let s: EventStream<string> = once("hello")
s.log()

As you can see, the global methods, such as once are imported separately.

Check out the new API Documentation, that's now generated using Typedoc from the Typescript source code.

Modern ES6 Browser, Node.js v.12+

You can directly import Bacon.js as single aggregated ES6 module.

import * as Bacon from 'node_modules/baconjs/dist/Bacon.mjs';
Bacon.once("hello").log();

NPM, CommonJS, Node.js

If you're on to CommonJS (node.js, webpack or similar) you can install Bacon using npm.

npm install baconjs

Try it like this:

node
Bacon=require("baconjs")
Bacon.once("hello").log()

The global methods, such as once are available in the Bacon object.

Bower

For bower users:

bower install bacon

CDN / Script Tags

Both minified and unminified versions available on cdnjs.

So you can also include Bacon.js using

<script src="https://cdnjs.cloudflare.com/ajax/libs/bacon.js/2.0.9/Bacon.js"></script>
<script>
Bacon.once("hello").log()
</script>

AMD / require.js

Bacon.js is an UMD module so it should work with AMD/require.js too. Not tested lately though.

Github

Prefer to drink from the firehose? Download from Github master.

Intro

The idea of Functional Reactive Programming is quite well described by Conal Elliot at Stack Overflow.

Bacon.js is a library for functional reactive programming. Or let's say it's a library for working with events in EventStreams and dynamic values (which are called Properties in Bacon.js).

You can wrap an event source, say "mouse clicks on a DOM element" into an EventStream by saying

let $ = (selector) => document.querySelector(selector) 
var clickE = Bacon.fromEvent($("h1"), "click")

The $ helper function above could be replaced with, for instance, jQuery or Zepto.

Each EventStream represents a stream of events. It is an Observable, meaning that you can listen to events in the stream using, for instance, the onValue method with a callback. Like this:

clickE.onValue(() => alert("you clicked the h1 element") )

But you can do neater stuff too. The Bacon of Bacon.js is that you can transform, filter and combine these streams in a multitude of ways (see EventStream API). The methods map, filter, for example, are similar to same functions in functional list programming (like Underscore). So, if you say

let plusE = Bacon.fromEvent($("#plus"), "click").map(1)
let minusE = Bacon.fromEvent($("#minus"), "click").map(-1)
let bothE = plusE.merge(minusE)

.. you'll have a stream that will output the number 1 when the "plus" button is clicked and another stream outputting -1 when the "minus" button is clicked. The bothE stream will be a merged stream containing events from both the plus and minus streams. This allows you to subscribe to both streams with one handler:

bothE.onValue(val => { /* val will be 1 or -1 */ console.log(val) })

Note that you can also use the log method to log stream values to console:

bothE.log()

In addition to EventStreams, bacon.js has a thing called Property, that is almost like an EventStream, but has a "current value". So things that change and have a current state are Properties, while things that consist of discrete events are EventStreams. You could think mouse clicks as an EventStream and mouse cursor position as a Property. You can create Properties from an EventStream with scan or toProperty methods. So, let's say

let add = (x, y) => x + y
let counterP = bothE.scan(0, add)
counterP.onValue(sum => $("#sum").textContent = sum )

The counterP property will contain the sum of the values in the bothE stream, so it's practically a counter that can be increased and decreased using the plus and minus buttons. The scan method was used here to calculate the "current sum" of events in the bothE stream, by giving a "seed value" 0 and an "accumulator function" add. The scan method creates a property that starts with the given seed value and on each event in the source stream applies the accumulator function to the current property value and the new value from the stream.

Hiding and showing the result div depending on the content of the property value is equally straightforward

let hiddenIfZero = value => value == 0 ? "hidden" : "visible"
counterP.map(hiddenIfZero)
  .onValue(visibility => { $("#sum").style.visibility = visibility })

For an actual (though a bit outdated) tutorial, please check out my blog posts

API

Creating EventStreams and Properties

There's a multitude of methods for creating an EventStream from different sources, including the DOM, node callbacks and promises for example. See EventStream documentation.

Properties are usually created based on EventStreams. Some common ways are introduced in Property documentation.

Combining multiple streams and properties

You can combine the latest value from multple sources using combine, combineAsArray, combineWith or combineTemplate.

You can merge multiple streams into one using merge or mergeAll.

You can concat streams using concat or concatAll.

If you want to get the value of an observable but emit only when another stream emits an event, you might want to use sampledBy or its cousin withLatestFrom.

Latest value of Property or EventStream

One of the common first questions people ask is "how do I get the latest value of a stream or a property". There is no getLatestValue method available and will not be either. You get the value by subscribing to the stream/property and handling the values in your callback. If you need the value of more than one source, use one of the combine methods.

Bus

Bus is an EventStream that allows you to push values into the stream. It also allows plugging other streams into the Bus.

Event

There are essentially three kinds of Events that are emitted by EventStreams and Properties:

  • Value events that convey a value. If you subscribe using onValue, you'll only deal with values. Also map, filter and most of the other operators also deal with values only.
  • Error events indicate that an error has occurred. More on errors below!
  • End event is emitted at most once, and is always the last event emitted by an Observable.

If you want to subscribe to all events from an Observable, you can use the subscribe method.

Errors

Error events are always passed through all stream operators. So, even if you filter all values out, the error events will pass through. If you use flatMap, the result stream will contain Error events from the source as well as all the spawned stream.

You can take action on errors by using onError.

See also mapError, errors, skipErrors, Bacon.retry and flatMapError.

In case you want to convert (some) value events into Error events, you may use flatMap like this:

stream = Bacon.fromArray([1,2,3,4]).flatMap(function(x) {
  if (x > 2)
    return new Bacon.Error("too big")
  else
    return x
})

Conversely, if you want to convert some Error events into value events, you may use flatMapError:

myStream.flatMapError(function(error) {
  return isNonCriticalError(error) ? handleNonCriticalError(error) : new Bacon.Error(error)
})

Note also that Bacon.js operators do not catch errors that are thrown. Especially map doesn't do so. If you want to map things and wrap caught errors into Error events, you can do the following:

wrapped = source.flatMap(Bacon.try(dangerousOperation))

For example, you can use Bacon.try to handle JSON parse errors:

var jsonStream = Bacon
  .once('{"this is invalid json"')
  .flatMap(Bacon.try(JSON.parse))

jsonStream.onError(function(err) {
  console.error("Failed to parse JSON", err)
})

An Error does not terminate the stream. The method endOnError returns a stream/property that ends immediately after the first error.

Bacon.js doesn't currently generate any Error events itself (except when converting errors using fromPromise). Error events definitely would be generated by streams derived from IO sources such as AJAX calls.

See retry for retrying on error.

Introspection and metadata

Bacon.js provides ways to get some descriptive metadata about all Observables.

See toString, deps, desc, spy.

Changes to earlier versions

Function Construction rules removed in 3.0

Function construction rules, which allowed you to use string shorthands for properties and methods, were removed in version 3.0, as they are not as useful as they used to be, due to the moderd, short lambda syntax in ES6 and Typescript, as well as libraries like Ramda and partial.lenses.

Lazy evaluation removed in 2.0

Lazy evaluation of event values has been removed in version 2.0

Cleaning up

As described above, a subscriber can signal the loss of interest in new events in any of these two ways:

  1. Return noMore from the handler function
  2. Call the dispose() function that was returned by the subscribe or onValue call.

Based on my experience, an actual side-effect subscriber in application-code almost never does this. Instead you'll use methods like takeUntil to stop listening to a source when something happens.

EventStream and Property semantics

The state of an EventStream can be defined as (t, os) where t is time and os the list of current subscribers. This state should define the behavior of the stream in the sense that

  1. When a Next event is emitted, the same event is emitted to all subscribers
  2. After an event has been emitted, it will never be emitted again, even if a new subscriber is registered. A new event with the same value may of course be emitted later.
  3. When a new subscriber is registered, it will get exactly the same events as the other subscriber, after registration. This means that the stream cannot emit any "initial" events to the new subscriber, unless it emits them to all of its subscribers.
  4. A stream must never emit any other events after End (not even another End)

The rules are deliberately redundant, explaining the constraints from different perspectives. The contract between an EventStream and its subscriber is as follows:

  1. For each new value, the subscriber function is called. The new value is wrapped into a Next event.
  2. The subscriber function returns a result which is either noMore or more. The undefined value is handled like more.
  3. In case of noMore the source must never call the subscriber again.
  4. When the stream ends, the subscriber function will be called with and End event. The return value of the subscribe function is ignored in this case.

A Property behaves similarly to an EventStream except that

  1. On a call to subscribe, it will deliver its current value (if any) to the provided subscriber function wrapped into an Initial event.
  2. This means that if the Property has previously emitted the value x to its subscribers and that is the latest value emitted, it will deliver this value to the new subscriber.
  3. Property may or may not have a current value to start with. Depends on how the Property was created.

Atomic updates

Bacon.js supports atomic updates to properties for solving a glitches problem.

Assume you have properties A and B and property C = A + B. Assume that both A and B depend on D, so that when D changes, both A and B will change too.

When D changes d1 -> d2, the value of A a1 -> a2 and B changes b1 -> b2 simultaneously, you'd like C to update atomically so that it would go directly a1+b1 -> a2+b2. And, in fact, it does exactly that. Prior to version 0.4.0, C would have an additional transitional state like a1+b1 -> a2+b1 -> a2+b2

For jQuery users

Earlier versions of Bacon.js automatically installed the asEventStream method into jQuery. Now, if you still want to use that method, initialize this integration by calling Bacon.$.init($) .

For RxJs Users

Bacon.js is quite similar to RxJs, so it should be pretty easy to pick up. The major difference is that in bacon, there are two distinct kinds of Observables: the EventStream and the Property. The former is for discrete events while the latter is for observable properties that have the concept of "current value".

Also, there are no "cold observables", which means also that all EventStreams and Properties are consistent among subscribers: when an event occurs, all subscribers will observe the same event. If you're experienced with RxJs, you've probably bumped into some wtf's related to cold observables and inconsistent output from streams constructed using scan and startWith. None of that will happen with bacon.js.

Error handling is also a bit different: the Error event does not terminate a stream. So, a stream may contain multiple errors. To me, this makes more sense than always terminating the stream on error; this way the application developer has more direct control over error handling. You can always use endOnError to get a stream that ends on the first error!

Examples

See Examples

See Specs

See Worzone demo and source

Build

First check out the Bacon.js repository and run npm install.

Then build the Typescript sources into a javascript bundle (plus typescript type definitions):

npm run dist

Result javascript files will be generated in dist directory. If your planning to develop Bacon.js yourself, you'll want to run [tests] too using npm test.

Test

Run all unit tests:

npm test

The tests are run against the javascript bundle in the dist directory. You can build the bundle using npm run dist.

This will loop thru all files under spec and build the library with the single feature and run the test.

Run browser tests locally:

npm install
npm run browsertest-bundle
npm rum browsertest-open

Run performance tests:

performance/PerformanceTest.coffee
performance/PerformanceTest.coffee flatmap

Run memory usage tests:

coffee --nodejs '--expose-gc' performance/MemoryTest.coffee

Dependencies

Runtime: none Build/test: see [package.json].

Compatibility with other libs

Bacon.js doesn't mess with prototypes or the global object, except that it exports the Bacon object as window.Bacon when installed using the <script> tag.

So, it should be pretty much compatible and a nice citizen.

I'm not sure how it works in case some other lib adds stuff to, say, Array prototype, though. Maybe add test for this later?

Compatibility with browsers

TLDR: good.

Bacon.js is not browser dependent, because it is not a UI library. It should work on all ES5-ish runtimes.

Automatically tested on each commit on modern browsers in Browserstack.

Why Bacon?

Bacon.js exists largely because I got frustrated with RxJs, which is a good library, but at that time didn't have very good documentation and wasn't open-source. Things have improved a lot in the Rx world since that. Yet, there are still compelling reasons to use Bacon.js instead. Like, for instance,

  • more consistent stream/property behavior
  • simplicity of use
  • atomic updates

If you're more into performance and less into atomic updates, you might want to check out Kefir.js!

Contribute

Use GitHub issues and Pull Requests.

Note:

  • the dist/Bacon*.js files are assembled from files in src/. After updating source files, run npm install to update the generated files. Then commit and create your Pull Request.
  • the API docs are generated from this README and docstrings in the sources in the src directory. See the baconjs.github.io repository for more info.

Sponsors

Thanks to BrowserStack for kindly providing me with free of charge automatic testing time.

Thanks also to Reaktor for supporting Bacon.js development and letting me use some of my working hours on open-source development.

NPM DownloadsLast 30 Days