Echo

A tiny reactive programming library

Echo is a 2kb (unminified) JavaScript library for reactive programming. The name comes from the fact that changes to the reactive values in Echo propagate throughout the system like an echo.

Echo was created as a personal exercise project, and is used in the Weatherwave app. I created Echo to study the design of signals as implemented in S.js, VueJS, and similar libraries.

Where do I get it?

Since Echo is a personal toy project, it is not published on NPM. However, you can use it by importing it directly from my server:

import {reactive, watch} as echo from 'https://stuff.yamasakivukelic.com/echo/versions/echo-v2.1.0.js'

Versions

How does it work?

The library exposes two functions, reactive() and watch().

Note: In the following examples, you will see variables that are suffixed with $, like x$. This is a naming convention among programmers using reactive stream libraries. Variables that point to reactive values are suffixed with $ (which stands for "stream").

Basic reactive programming

Reactive containers allow the watchers to observe changes to values. You put something in a reactive container, and when you access them inside a watcher, the watcher automatically starts tracking them. The next time you change the contents of a reactive container, all watchers that are tracking them will re-run on their own.

Reactive containers are created using the reactive() function:

var x$ = reactive(1)

The value passed to the function is optional and it is used as the initial value. The default is undefined.

The value of the reactive container is accessed using its .value property. You can also assign to the property to update the value.

console.log(x$.value) // get value
x$.value++ // update value

Updating the value in and of itself doesn't do anything. To do reactive programming, you need a way to observe the changes in the value.

To observe the values you create watchers. Watchers are operations over one or more reactive values. The reactive values they operate on are called dependencies. Whenever a dependency updates, the watcher is re-run automatically.

var x$ = reactive(1)
var y$ = reactive(2)
var sum$ = watch(function () {
	return x$.value + y$.value
})

Watchers themselves also behave like read-only reactive containers. The value of the watcher is the return value of the function it runs. Using the above example, I get:

console.log(sum$.value) // => 3

Changed in v2.1.0 The dependency updates are queued so updating multiple dependencies will not cause the watcher to re-run multiple times. When accessing a watcher value with queued-up updates, it will immediately re-run and flush the queue so that the value is up-to-date.

Watchers can be also used purely for their side-effect. For example I can use the above sum$ watcher to update the DOM:

watch(function () {
	document.getElementById('output').textContent = sum$.value
})

This watcher will re-run every time the sum$ watcher is updated, which is, in turn, every time either the x$ or the y$ reactive values are updated. Notice how I don't explicitly track dependencies.

Cyclical updates

A cyclical update happens when a watcher updates one of its dependencies. For example:

var x$ = reactive(1)
watch(function () {
	x$.value++
})

This will cause an infinite loop the next time x$ is updated. You should avoid updating dependencies within a watcher. This is especially tricky when the dependencies are not direct dependencies of the watcher. Currently Echo does not have any means of detecting cyclical updates/dependencies.

Asyncrhonous reactive programming

While the watcher is able to capture all dependencies automatically when I am using synchronous functions, it cannot do so for asyncrhonous code. For example, consider the following:

var products$ = watch(function () {
	if (!$keyword.value) return
	searchProducts(keyword$.value)
		.then(function (results) {
			return paginate(results)
		})
})

There are two ways in which I can handle this. One is to make the promise the value of the products$ watcher.

var products$ = watch(function () {
	if (!$keyword.value) return
	return searchProducts(keyword$.value)
		.then(function (results) {
			return paginate(results)
		})
})

We can also use a second reactive value to store the result.

var product$ = reactive()
watch(function () {
	searchProducts(keyword$.value)
		.then(function (results) {
			products$.value = paginate(results)
		})
})

Reduce pattern

Some reactive programming libraries provide a mechanism similar to Array.prototype.reduce(). Echo does not explicitly provide such functionality, but it can be implemented using closures and IIFE.

var x$ = reactive(1)
var sum$ = watch((function () {
	var sum = 0
	return function () {
		return sum += x$.value
	}
}()))

In the example, the plain non-reactive value sum is used as the accumulator. The expression sum += x$.value adds x$.value to the current value of sum and then evaluates the the new value of the variable. It is then returned from the watcher function to update the value of the sum$ watcher.

Disposing of watchers

In some situations, it is necessary to dispose of the watchers to prevent memory leaks.

For elements that are removed from the DOM tree, the simplest way to dispose of the watcher is to call the watch.dispose() function within the watcher function.

watch(function () {
	if (!document.body.contains($output)) watch.dispose()
	$output.textContent = input$.value
})

In the first line, I check whether the DOM tree still contains the target element. If it's no longer in the DOM tree, I invoke watch.dispose() to dispose of the watcher.

Sometimes we know exactly when we need to dispose. For example, in lifecycle hooks of various component-based libraries or native custom elements. In such cases, we can simply call the .dispose() method on the watcher object.

disconnectedCallback() {
	this.watcher$.dispose()
}

Example

The following is a list of examples on Codepen.