Cyclic Reactive Dependencies
Tags: webdev, javascript, reactivity, architecture
Published: 2024-02-04 07:18:00

Sometimes we can end up with cyclical dependencies. Sometimes we may want to do them intentionally. For example, when implementing a converter between degrees Celsius and Fahrenheit, where the user can change any of the two values, and the second should be recalculated automatically.

However, in the vast majority of cases, cyclic dependencies indicate a problem with logic, so they are usually avoided. Fortunately, the logic of even a degree converter can always be rewritten so that there are no cyclic dependencies.

So let's look at how different systems react to this emergency situation.

> 🚫 Unreal: Impossible
> 💤 Infinite: Endless loop
> 🎰 Limbo: Arbitrary result
> 🌋 Fail: Causes an error

# 🚫 Unreal

It's quite tempting to make it syntactically impossible to create loops. For example, we can require, when creating a state, that all of its dependencies already exist. This is typically the case with push libraries.

![](https://raw.githubusercontent.com/nin-jin/slides/master/reactivity/reactivity-cycle-unreal.svg)

It doesn't sound bad. However, we threw out the baby with the bathwater. That is, we have extremely limited ourselves in what kind of logic of invariants we are able to describe. In particular, this practically puts an end to the dynamic configuration of data streams. For example, it will no longer be possible to implement a spreadsheet on such an architecture.

# 💤 Infinite

A number of libraries simply go into an endless loop, constantly updating the same states.

![](https://raw.githubusercontent.com/nin-jin/slides/master/reactivity/reactivity-cycle-allow.svg)

For Angular and React, for example, this is typical behavior. There is even a crutch - a limit on the number of recalculations of one invariant. But we'll talk about this later.

# 🎰 Limbo

There is also a very strange solution - when indirectly accessing the state that is currently being calculated, its previous value is used.

![](https://raw.githubusercontent.com/nin-jin/slides/master/reactivity/reactivity-cycle-limbo.svg)

Depending on the order of calculations, this approach gives different results. That is, not only does the state turn out to be inconsistent, but also the behavior of the application becomes not stable, but begins to depend on the weather on Mars.

# 🌋 Fail

The best solution is to detect the loop at runtime and throw an exception.

![](https://raw.githubusercontent.com/nin-jin/slides/master/reactivity/reactivity-cycle-fail.svg?)

Further processing proceeds in the same way as with any other emergency situations. So it is especially important here that the system handles exceptions correctly.

# Cycles Cutting Practice 

As the application runs, dependencies are constantly rearranged to the point of being in diametrically opposite directions. For example, let's take a temperature converter, where, depending on what temperature we set explicitly, the second temperature should be calculated as a derived state. A naive implementation might look something like this:

```ts
class Converter extends Object {

  @mem fahrenheit( fahrenheit?: number ) {
    return fahrenheit ?? this.celsius() * 9 / 5 + 32
  }

  @mem celsius( celsius?: number ) {
    return celsius ?? ( this.fahrenheit() - 32 ) * 5 / 9
  }

}
```

```ts
const conv = new Converter

conv.fahrenheit( 32 ) // 32 ✅
conv.celsius() // 0 ✅
```

But there are two problems here. The first is infinite recursion if you access one of the properties without setting the value of at least one of them. With conventional methods, this would crash the browser, eat up a lot of memory, and crash with a stack overflow. But we have memoized methods - for a specific method called with specific keys in a specific object, they create a unique persistent atom. So, repeatedly accessing an atom while it is already being evaluated means an infinite recursion, which we can stop immediately, avoiding cyclic dependencies:


```ts
const conv = new Converter
conv.celsius() // Error: Circular subscription ❌
```

At the same time, if the method calls differ in any way, then there is no infinite recursion. For example, we can give the classic recursive calculation of the Fibonacci number:

```ts
class Fibonacci extends Object {

  @mems static value( index ) {
    if( index < 2 ) return 1
    return this.value( index - 2 ) + this.value( index - 1 )
  }

}
```

Thanks to memoization, even the thousandth number can be calculated instantly. But the price for this, of course, is the creation of thousands of caching atoms.

Another problem is two conflicting sources of truth. When we set the values of both states, both become primary and not necessarily consistent:

```ts
const conv = new Converter

conv.fahrenheit( 32 ) // 32 ✅
conv.celsius() // 0 ✅

conv.celsius(32) // 32 ✅
conv.fahrenheit() // 32 ❌
```

Correcting this code is not difficult - just move the source of truth into a separate property, and make both of ours derivative:

![](https://habrastorage.org/webt/lt/yz/68/ltyz68b-gyfkqbwtubx8pdsvcso.png)

```ts
class Converter extends $mol_object2 {

  @mem source( value = { celsius: 0 } ) {
    return value
  }

  @mem fahrenheit( fahrenheit ) {
    const source = this.source( fahrenheit?.valueOf && { fahrenheit } )
    return source.fahrenheit ?? source.celsius * 9 / 5 + 32
  }

  @mem celsius( celsius ) {
    const source = this.source( celsius?.valueOf && { celsius } )
    return source.celsius ?? ( source.fahrenheit - 32 ) * 5 / 9
  }

}
```

```ts
const conv = new Converter

conv.celsius() // 0 ✅
conv.fahrenheit() // 32 ✅

conv.fahrenheit( 0 ) // 0 ✅
conv.celsius() // -18 ✅

conv.celsius( 0 ) // 0 ✅
conv.fahrenheit() // 32 ✅
```

However, combining sources of truth is not always possible.