DEV Community

Cover image for Tonus of Reactivity

Posted on • Originally published at

Tonus of Reactivity

You can calculate dependent states as early as possible, or as late as possible, even to the point of abandoning calculations, if possible.

🍔 Instant: Instant reactions
⏰ Defer: Delayed reactions
🦥 Lazy: Lazy calculations

🍔 Instant

In libraries such as RxJS, dependent states are recalculated immediately when the dependency changes. If we need to change several states in a row, this may lead to unnecessary calculations.

Moreover, these extra intermediate calculations produce an inconsistent state, calculated partly from states that have already been updated, and partly from states that have not yet been updated. And an inconsistent state, even if temporary, is a very dangerous thing. At best, the user will observe glitches - visual flickering. At worst, the application will not work correctly and contain various errors.

⏰ Defer

To avoid glitches the recalculation can be delayed until later, so that it is performed only once, no matter how many dependencies are updated.

However, the recalculation will be carried out in any case, even if the result is not useful to us.

🦥 Lazy

In drag-on reactivity models, it is possible to lazily evaluate invariants - only at the moment when the dependent state is actually needed.

When the initial states change, we do not calculate the dependent ones and do not even schedule their calculation, but only mark them as outdated. And if you subsequently access them, they will begin to be calculated.

This is both the most economical approach and the most consistent, since it guarantees that whenever we access the state, the resulting value will be relevant.

Lazyness in $mol_wire

Let's take a simple application with a stabilized state.

Subscribers at the top, publishers at the bottom. The arrows show the movement of data. Typically, an application has one root, which acts as the application's starting point.

When any state changes, immediately, synchronously, all direct dependencies are marked as “stale”, and all indirect dependencies as “doubt”.

This is necessary so that each state knows its relevance status at any given time. There are 4 such statuses in total:

  • 🔴 stale (-1) - the value is outdated and requires recalculation (initial state).
  • 🟡 doubt (-2) - among the indirect dependencies there are outdated values, they need to be updated.
  • 🟢 fresh (-3) - the value is relevant and can be returned immediately.
  • 🔵 final (-4) - the value will never change again (the task is completed or the atom is destroyed).

The status is stored in the cursor field. Negative values encode the current status, and non-negative values indicate that the fiber is currently being evaluated and is keeping track of its dependencies. A positive cursor value means how many publishers have already been tracked by this point.

Propagating status change notifications to the application root can be a resource-intensive operation. Especially when thousands of others depend on one condition. But this is a reasonable price to pay for guaranteeing consistency.

If we immediately change other states, then the notification chains will be shorter, since they will run into an already outdated subgraph.

So far we have only changed the statuses of states. If, for any reason, we want to read the value of an stale or doubt state, then only at that moment the calculations will begin.

First, there is a dive through the doubts to the stale ones, which calculate the new value. If the value has changed, then all dependencies also become outdated and begin to be updated. And so on the calculations rise to the value we requested. Or they stop somewhere halfway, and all the states above are marked as relevant.

Thus, despite the delayed lazy calculations, we can access any state at any time and get the current value, consistent with all indirect dependencies.

Finally, even if we have not explicitly requested any state, an automatic delayed update of all roots occurs strictly in the same order in which they were calculated at the start.

This guarantees that even if the state graph is rearranged during the recalculation process (which happens quite often), no state will be simply discarded after calculation, since the state that affects its existence will always be calculated earlier.

Since the user will still not see the result more often than the FPS of the screen, it makes sense to postpone the automatic updating of states to the next animation frame. Thus, all state changes within one frame are grouped together and lead to only one rerender. And all recalcs happen only when someone needs them.

An interesting feature of using animation frames is that when the interface is not visible, its rendering freezes automatically, without wasting resources on invisible work. However, some tasks still require work even in the background. These just need to be touched according to the time interval you need.

Top comments (0)