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.
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'
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").
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.
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.
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)
})
})
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.
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()
}
The following is a list of examples on Codepen.