npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@lume/variable

v0.10.1

Published

Create reactive variables and react their changes.

Downloads

107

Readme

@lume/variable

Make reactive variables and react to their changes.

npm install @lume/variable --save

React to changes in reactive variables

In the following we make a reactive variable count, increment its value every second, and re-run a piece of code that logs the value to the console on each change:

import {variable, autorun} from '@lume/variable'

const count = variable(0)

setInterval(() => count(count() + 1), 1000)

autorun(() => {
	// Log the count variable any time it changes.
	console.log(count())
})

The function passed into autorun (sometimes referred to as "an autorun" or "a computation") automatically re-runs every second due to count being incremented every second.

Calling count() gets the current value, while calling count(123) with an arg sets the value. Thus count(count() + 1) increments the value.

Any reactive variables used inside an autorun function are registered (or "tracked") as "dependencies" by autorun, then any time those dependencies change, the autorun re-runs.

We call this "dependency-tracking reactivity".

An autorun with multiple variables accessed inside of it will re-run any time any of the accessed variables change:

autorun(() => {
	// Log these variables every time any of them change.
	console.log(firstName(), lastName(), age(), hairColor())
})

autoruns can be grouped in any way we like:

autorun(() => {
	// This re-runs only when firstName or lastName have changed.
	console.log(firstName(), lastName())
})

autorun(() => {
	// This re-runs only when age or hairColor have changed.
	console.log(age(), hairColor())
})

If we wish to stop an autorun from re-running, we can call its returned stop function (note, this is not necessary if we or the JS engine no longer have references to any of the reactive variables that are dependencies of the autorun, and in that case everything will be garbage collected and will no longer re-run):

import {variable, autorun} from '@lume/variable'

const count = variable(0)

setInterval(() => count(count() + 1), 1000)

const stop = autorun(() => {
	// Log the count variable any time it changes.
	console.log(count())
})

// Stop the autorun (and therefore no more logging will happen) after 5 seconds:
setTimeout(stop, 5000)

Power and Simplicity

Learn how dependency-tracking reactivity makes your code cleaner and more concise compared to another more common pattern.

Reactive computations (autoruns) are nice because it doesn't matter how we group our variables (dependencies) within computations. What matters is we write what we care about (expressions using our variables) without having to think about how to wire reactivity up.

With an event-based pattern, in contrast, our code would be more verbose and less convenient.

Looking back at our simple autorun for logging several variables,

autorun(() => {
	// Log these variables every time any of them change.
	console.log(firstName(), lastName(), age(), hairColor())
})

we will see that writing the same thing with some sort of event pattern is more verbose:

function log() {
	// Log these variables every time any of them change.
	console.log(firstName.value, lastName.value, age.value, hairColor.value)
}

// We need to also register an event handler for each value we care to react to:
firstName.on('change', log)
lastName.on('change', log)
age.on('change', log)
hairColor.on('change', log)

With this hypothetical event pattern, we had to share our logging function with each event emitter in order to wire up the reactivity, having us write more code. Using autorun was simpler and less verbose.

Now let's say we want to add one more item to the console.log statement.

Here is what that looks like with an autorun:

autorun(() => {
	// Log these variables every time any of them change.
	console.log(firstName(), lastName(), age(), hairColor(), favoriteFood())
})

With an event emitter pattern, there is more to do:

function log() {
	// Log these variables every time any of them change.
	console.log(firstName.value, lastName.value, age.value, hairColor.value, favoriteFood.value)
}

firstName.on('change', log)
lastName.on('change', log)
age.on('change', log)
hairColor.on('change', log)
favoriteFood.on('change', log) // <-------- Don't forget to add this line too!

Not only is the event pattern more verbose, but it is more error prone because we can forget to register the event handler: we had to modify the code in two places in order to add logging of the favoriteFood value.

Here's where it gets interesting!

Reactive computations allow us to decouple the reactivity implementation from places where we need reactivity, and to focus on the code we want to write.

Let's say we want to make a class with properties, and abserve any of them when they change.

First, let's use a familiar event pattern to show the less-than-ideal scenario first:

// Let's say this is in a lib called 'events'.
class EventEmitter {
	addEventHandler(eventName, fn) {
		/*...use imagination here...*/
	}
	removeEventHandler(eventName, fn) {
		/*...use imagination here...*/
	}
	emit(eventName, data) {
		/*...use imagination here...*/
	}
}

Now let's use EventEmitter to make a class whose poperties we can observe the changes of. In the following class, we'll make getter/setter pairs so that any time a setter is used to set a value, it will emit a "change" event.

import {EventEmitter} from 'events'

// We need to extend from EventEmitter (or compose it inside the class, but the amount
// of code would be similar).
class Martian extends EventEmitter {
	_firstName = ''
	get firstName() {
		return this._firstName
	}
	set firstName(v) {
		this._firstName = v
		this.emit('change', 'firstName') // Emit any time the property is set.
	}

	_lastName = ''
	get lastName() {
		return this._lastName
	}
	set lastName(v) {
		this._lastName = v
		this.emit('change', 'lastName')
	}

	_age = 0
	get age() {
		return this._age
	}
	set age(v) {
		this._age = v
		this.emit('change', 'age')
	}

	_hairColor = ''
	get hairColor() {
		return this._hairColor
	}
	set hairColor(v) {
		this._hairColor = v
		this.emit('change', 'hairColor')
	}

	_favoriteFood = ''
	get favoriteFood() {
		return this._favoriteFood
	}
	set favoriteFood(v) {
		this._favoriteFood = v
		this.emit('change', 'favoriteFood')
	}
}

const martian = new Martian()

The following shows how we would react to changes in three of the five properties of a Martian:

martian.addEventHandler('change', property => {
	if (['firstName', 'hairColor', 'favoriteFood'].includes(property)) {
		// Log these three variables every time any of the three change.
		console.log(martian.firstName, martian.hairColor, martian.favoriteFood)
	}
})

It works, but we can still make this better while still using the same event pattern.

Let's say we want to make it more efficient: instead of all event handlers being subscribed to a single change event (because Martians probably have lots and lots of properties) and filtering for the properties we care to observe, we can choose specific event names for each property and subscribe handlers to specific property events:

import {EventEmitter} from 'events'

class Martian extends EventEmitter {
	_firstName = ''
	get firstName() {
		return this._firstName
	}
	set firstName(v) {
		this._firstName = v
		this.emit('change:firstName') // Emit a specific event for the firstName property.
	}

	_lastName = ''
	get lastName() {
		return this._lastName
	}
	set lastName(v) {
		this._lastName = v
		this.emit('change:lastName') // Similar for the lastName property.
	}

	_age = 0
	get age() {
		return this._age
	}
	set age(v) {
		this._age = v
		this.emit('change:age') // And so on.
	}

	_hairColor = ''
	get hairColor() {
		return this._hairColor
	}
	set hairColor(v) {
		this._hairColor = v
		this.emit('change:hairColor')
	}

	_favoriteFood = ''
	get favoriteFood() {
		return this._favoriteFood
	}
	set favoriteFood(v) {
		this._favoriteFood = v
		this.emit('change:favoriteFood')
	}
}

We can now avoid the overhead of the array filtering we previously had with the .includes check:

const martian = new Martian()

const onChange = () => {
	// Log these three variables every time any of the three change.
	console.log(martian.firstName, martian.hairColor, martian.favoriteFood)
}

martian.addEventHandler('change:firstName', onChange)
martian.addEventHandler('change:hairColor', onChange)
martian.addEventHandler('change:favoriteFood', onChange)

This is better than before because now if other properties besides the ones we've subscribed to change, the event pattern won't be calling our function needlessly and we won't be doing property name checks every time.

We can still do better with the event pattern! (Spoiler: it won't get as clean as with autorun below, which we'll get to next.)

We can come up with an automatic event-wiring mechanism. It could look something like the following:

import {EventEmitter, WithEventProps} from 'events'

// Imagine `WithEventProps` wires up events for any properties specified in a
// static `eventProps` field:
const Martian = WithEventProps(
	class Martian extends EventEmitter {
		static eventProps = ['firstName', 'lastName', 'age', 'hairColor', 'favoriteFood']

		firstName = ''
		lastName = ''
		age = 0
		hairColor = ''
		favoriteFood = ''
	},
)

// Listen to events as before:

const martian = new Martian()

const onChange = () => {
	// Log these three variables every time any of the three change.
	console.log(martian.firstName, martian.hairColor, martian.favoriteFood)
}

martian.addEventHandler('change:firstName', onChange)
martian.addEventHandler('change:hairColor', onChange)
martian.addEventHandler('change:favoriteFood', onChange)

That is a lot shorter already, but we can still do better! (It still won't be as simple as with dependency-tracking reactivity, which is coming up.)

We can make the event pattern more DRY ("Don't Repeat Yourself") using decorators to allow us to be less repetitive:

import {EventEmitter, emits} from 'events'

// Imagine this `@emits` decorator wires up an event for each decorated property.
@emits
class Martian extends EventEmitter {
	@emits firstName = ''
	@emits lastName = ''
	@emits age = 0
	@emits hairColor = ''
	@emits favoriteFood = ''
}

// Listen to events as before:

const martian = new Martian()

const onChange = () => {
	// Log these three variables every time any of the three change.
	console.log(martian.firstName, martian.hairColor, martian.favoriteFood)
}

martian.addEventHandler('change:firstName', onChange)
martian.addEventHandler('change:hairColor', onChange)
martian.addEventHandler('change:favoriteFood', onChange)

This is better than before because now we didn't have to repeat the property names twice, reducing the chance of errors from mismatched names. Instead we labeled them all with a decorator.


We can still do better! 🤯

With LUME's reactive variables we can further decouple a class's implementation from the reactivity mechanism and make things cleaner.

We can re-write the previous non-decorator example (and still not using decorators) so that our class does not need to extend from a particular base class to inherit a reactivity implementation:

import {variable, autorun} from '@lume/variable'

// This class does not extend from any base class. Instead, reactive variables
// are defined inside the class.
class Martian {
	firstName = variable('')
	lastName = variable('')
	age = variable(0)
	hairColor = variable('')
	favoriteFood = variable('')
}

const martian = new Martian()

autorun(() => {
	// Log these three variables every time any of the three change.
	console.log(martian.firstName(), martian.hairColor(), martian.favoriteFood())
})

This is better than before because the reactivity is not an inherent part of our class hierarchy, instead being a feature of the reactive variables. We can use this form of reactivity in our Matrian class or in any other class without having class inheritance requirements, and other developers do not have to make subclasses of our classes just to have reactivity.

Plus, we did not need to subscribe an event listener to specific events like we did earlier with the addEventHandler calls. Instead, we wrapped our function with autorun and it became a "reactive computation" with the ability to re-run when its dependencies (the reactive variables used within it) change.

...We can still do better! 🤯...

Using LUME's decorators, the experience is as good as it gets:

import {variable, autorun, reactive} from '@lume/variable'

// Now we mark the class and properties as reactive with the `@reactive` decorator.
@reactive
class Martian {
	@reactive firstName = ''
	@reactive lastName = ''
	@reactive age = 0
	@reactive hairColor = ''
	@reactive favoriteFood = ''

	// This property is not reactive, as it is not marked with `@reactive`.
	cryogenesis = false
}

const martian = new Martian()

autorun(() => {
	// Log these four variables every time any of the first three change. Note
	// that this will not automatically rerun when cryogenesis changes because cryogenesis
	// is not reactive.
	console.log(martian.firstName, martian.hairColor, martian.favoriteFood, martian.cryogenesis)
})

This is better than before because now we can use the properties like regular properties instead of having to call them as functions to read their values like we had to in the prior example. We can write this.age instead of this.age() for reading a value, and this.age = 10 instead of this.age(10) for writing a value.

Dependency-tracking reactivity makes things nice and concise.

API

const myVar = variable(value)

Creates a reactive variable with an initial value. The return value is a function that

  • when called with no argument, returns the reactive variable's value, f.e. myVar().
  • when called with an argument, sets the reactive variable's value, f.e. myVar(newValue).
const foo = variable(false)
const bar = variable(123)

const stop = autorun(fn)

Takes a function fn and immediately runs it, while tracking any reactive variables that were used inside of it as dependencies. Any time those variables change, fn is executed again. Each time fn re-runs, dependencies are re-tracked, which means that conditional branching within fn can change which dependencies will re-run fn next time.

autorun returns a stop function that when called causes fn never to be automatically executed again. This is useful when you no longer care about some variables.

autorun(() => {
	if (foo()) doSomethingWith(bar())
})

@reactive

A decorator that makes properties in a class reactive. Besides decorating properties with the decorator, also be sure to decorate the class that shall have reactive variable with the same decorator as well.

@reactive
class Car {
	@reactive engineOn = false
	@reactive sound = 'vroom'
}

const car = new Car()

autorun(() => {
	// Any time the car turns on, it makes a sound.
	if (car.engineOn) console.log(car.sound)
})