DEV Community

Sam Holmes
Sam Holmes

Posted on

Reactive Programming in JavaScript

Reactive programming is a declarative programming paradigm concerned with the propagation of change. If you'd like a full explanation I'd recommend reading Paul Stovell's article, What is Reactive Programming? In this article, I will show you how you can implement a reactive programming environment in JavaScript.

State as a Graph

In order to accomplish reactive programming in JavaScript, we will need to manage our state on our own and construct a dependency graph for our variables. This way, when a variable's state changes, we propagate that change to all other variables that depend on that state. For example:

a = 10
b = a + 1
c = a + 2

This code would construct the following graph:

   a
  / \
 b   c

The graph's purpose is to establish a dependency relationship between our variables. This way, when a changes, we know to re-evaluate (or update) b and c. In other words, b and c are dependent on a.

We'll implement our graph using an object that maps a dependency variable's reference to a set of dependent references:

let depgraph = {}

depgraph[a] = {b: true, c: true}

To make our lives easier we can create addDependent and removeDependent functions to add and remove dependents in the graph.

// Adds a dependent to the depgraph
function addDependent(dependency, dependent) {
  depgraph[dependency] = depgraph[dependency] || {}
  depgraph[dependency][dependent] = true
}
// Removes a dependent from the depgraph
function removeDependent(dependency, dependent) {
  depgraph[dependency] = depgraph[dependency] || {}
  delete depgraph[dependency][dependent]
}

State

In our program, variables will hold the reference to their state rather than the value of their state. We will use Symbol() to create unique references for our variables. This guarantees that our references don't have any collisions with each other.

However, we will need a place to store the actual values of our variables (a cache). So, we will use an object to map references to values:

let state = {}

state[a] = 10
...

_Notice that a is not a string; this is cause it is equal to a unique Symbol for the variable.

Updaters

Now we need a way to evaluate variables at any point in time. So, we will need to maintain an "updater" function for each variable in order to re-evaluate a variable's state.

let updaters = {}

updaters[a] = () => 10
updaters[b] = () => state[a] + 1
updaters[c] = () => state[c] + 2

You can think of the updaters as storing the right-side expression of the assignment for each variable. Now at any point we can invoke a variable’s updater to retrieve its state.

Let’s bring it all together. We have a graph that maps out the dependency relationships. We have the state stored in a single location. And we have a set of updater functions. When a variable’s state changes, we want to find all of its dependents in our graph and run each of their updater functions in order to get the new state. We of course recursively continue this process for any dependents of those variables as well. To do this, let’s construct a series of functions that will be our reactive programming API.

The API

Our API will consist of a set of functions: declare, bind, update, reorg, retrieve, and remove. To understand how we will be using the API, let’s look at the native JavaScript equivalents to each function:

// Variable declaration
let a = true
// becomes
let a = declare(true)

// Variable assignment
a = false
// becomes
update(a, false)

// Variable assignment (with dependencies)
a = b + 1
// becomes
bind(a, () => retrieve(b) + 1)

// Value retrieval
console.log(a)
// becomes
console.log(retrieve(a))

// Finally, memory deallocation
// JavaScript's GC takes care of this for us,
// but we need to manually release our variables.
remove(a)

The function reorg will be used internally and has no native equivalency.

Let's get into the implementation details of each function.

declare

function declare(expr) {
  if (typeof expr === 'undefined') expr = () => undefined

  let ref = Symbol()

  return bind(ref, expr)
}

This function will allow us to declare a new variable and bind an expression to it using bind. This will replace our native variable declarations with the following:

let a = declare(10)
let b = declare(() => retrieve(a) + 1)
let c = declare(() => retrieve(a) + c)

bind

function bind(ref, expr) {
  updaters[ref] = () => update(ref, expr)
  reorg(ref)
  return ref
}

This function will be used to bind an expression to a reference.

We create an updater for the reference, invoke reorg, and then return the reference. It'll all make more sense as we go along. But the most important thing to note is that the updater is a function that updates the reference with the given expression.

reorg

function reorg(ref) {
  // Remove ref as a dependent to other refs in the graph
  // Effectively detaching it it from the graph
  Object.getOwnPropertySymbols(depgraph).forEach(dependency =>
    removeDependent(dependency, ref)
  )

  // Run the updater and retrieve the dependencies during the update
  let dependencies = updaters[ref]()

  // Update the graph using dependencies
  // Effectively, re-attaching the updated ref to the graph
  if (dependencies) {
    dependencies.forEach(dependency => addDependent(dependency, ref))
  }
}

The purpose of this function is to dynamically maintain dependency relationships between references. In other words, when ever a variable is defined (using declare or bind) we must establish it as a dependent on any variables in its expression.

This function will reorganize the graph given a single reference. First, it will detach the reference from the graph, run its updater function, and then reattach it to the graph. The updater function always returns the reference’s dependencies, so we know how it should be reconnected to the graph.

update

function update(ref, expr) {
  let dependencies

  // Set to object to effectively gather all state retrievals
  stateRecentlyAccessed = {}

  // Execute expression and set actual state
  state[ref] = typeof expr === 'function' ? expr() : expr

  // If statement prevents error (not sure why stateRecentlyAccessed is null sometimes)
  if (stateRecentlyAccessed)
    dependencies = Object.getOwnPropertySymbols(stateRecentlyAccessed)

  // Set stateRecentlyAccessed to null to turn off listening
  stateRecentlyAccessed = null

  // This is where we invoke dependent updaters
  if (depgraph[ref]) {
    Object.getOwnPropertySymbols(depgraph[ref]).forEach(reorg)
  }

  return dependencies
}

Now we get to the core or our implementation. This function will update the state and return all the dependencies of a reference's expression.

This is where you notice stateRecentlyAccessed. I admit that I forgot to mention this global. It should hold the references recently accessed using retrieve. It'll make more sense if we look at the retrieve function.

retrieve

function retrieve(ref) {
  if (stateRecentlyAccessed) {
    stateRecentlyAccessed[ref] = true
  }
  return state[ref]
}

This function simply retrieves the state for a reference, but it also has one side-effect. The side-effect here is modifying stateRecentlyAccessed. Anytime a reference's state is accessed, stateRecentlyAccessed is modified so that it contains a property using the reference as its property accessor. The stateRecentlyAccessed global variable is how update can return a list of dependencies and how the system is able to maintain the variable relationships dynamically.

remove

function remove(ref) {
  // Removes it from state and updaters
  delete state[ref]
  delete updaters[ref]

  // Removes it from depgraph
  Object.getOwnPropertySymbols(depgraph).forEach(dependency => {
    if (dependency === ref) {
      delete depgraph[dependency]
    } else {
      Object.getOwnPropertySymbols(depgraph[dependency]).forEach(dependent => {
        if (dependent === ref) {
          delete depgraph[dependency][dependent]
        }
      })
    }
  })
}

Finally, we need a way to remove a reference and clean up after it. Unfortunately, we can't take full advantage of JavaScript's garbage collector because references are always used in the global variables state, updaters, etc. So, we have to do manual clean up our reference variables using this function. It may be possible to implement a garbage collector of our own, but for simplicity I chose to leave that idea alone.

Using our API

Let's construct an example using our API.

let coordinates = declare('Move your mouse!')
let mouseX = declare()
let mouseY = declare()

bind(coordinates, `${retrieve(mouseX)},${retrieve(mouseY)}`)

document.addEventListener('mousemove', (ev) => {
  update(mouseX, ev.clientX)
  update(mouseY, ev.clientY)
})

declare(() => document.body.innerHTML = retrieve(coordinates))

In this example, we declare a coordinates variable as well as two others: mouseX and mouseY. We bind coordinates to an expression depending on mouseX and mouseY.

After this, we update mouseX and mouseY in the mousemove event. We don't need to use bind in this case because we know they wont have any retrieve() function calls. Using update is a bit more performant because it skips the reorg.

We also declare document.body.innerHTML to be equal to the coordinates. Notice we don't need the reference that this declaration returns. You could however use the reference to access the innerHTML state if it's used in another part of your program. For example,

let innerHTML = declare(() => document.body.innerHTML = retrieve(coordinates))

// Use innerHTML reference somewhere else...

Final Notes

You now have the tools necessary to write reactive programs. Some things to consider doing to improve the implementation:

  1. Better garbage collection.
  2. Use Proxy() to make the API more terse.
  3. Write a transpiler that abstracts away the API altogether.

All in all, I hope this acted as a good introduction to reactive programming.

Top comments (1)

Collapse
 
opowell profile image
opowell

I think there is a typo:

let c = declare(() => retrieve(a) + c)

should be
let c = declare(() => retrieve(a) + 2)

?