Recently, we introduced the notion of transactionally reactive programming - consistent propagation of changes from data layer to visual components.

This article is focused on the transactionally reactive state management in a browser-based Web application. The described concepts are implemented in the open-source JavaScript library, called Reactronic.

Transactional reactivity is based on the three fundamental concepts:

  • State - a set of objects that store data of an application;

  • Transaction - a function that changes state objects in an atomic way;

  • Cache - a computed value having associated function that is automatically called to renew the cache in response to state changes.

The following picture illustrates relationships between the concepts in the source code:

Reactronic

Below is the detailed description of each concept.

State

State is a set of objects that store data of an application. All state objects are transparently hooked to track access to their properties, both on reads and writes.

@state
class MyModel {
  url: string = "https://nezaboodka.com";
  content: string = "database reinvented";
  timestamp: Date = Date.now();
}

In the example above, the class MyModel is instrumented with the @state decorator and its properties url, content, and timestamp are hooked.

Transaction

Transaction is a function that changes state objects in an atomic way. Every transaction function is instrumented with hooks to provide transparent atomicity (by implicit context switching and isolation).

@state
class MyModel {
  // ...
  @transaction
  async load(url: string): Promise<void> {
    this.url = url;
    this.content = await fetch(url);
    this.timestamp = Date.now();
  }
}

In the example above, the function load is a transaction that makes changes to url, content and timestamp properties. While the transaction is running, the changes are visible only inside the transaction itself. The new values become atomically visible outside of the transaction only upon its completion.

Atomicity is achieved by making changes in an isolated data snapshot that is visible outside of the transaction (e.g. displayed on user screen) only when it is finished. Multiple objects and their properties can be changed with full respect to the all-or-nothing principle. To do so, separate data snapshot is automatically maintained for each transaction. That is a logical snapshot that does not create a full copy of all the data.

Compensating actions are not needed in case of the transaction failure, because all the changes made by the transaction in its logical snapshot are simply discarded. In case the transaction is successfully committed, affected caches are invalidated and corresponding caching functions are re-executed in a proper order (but only when all the data changes are fully applied).

Asynchronous operations (promises) are supported out of the box during transaction execution. The transaction may consist of a set of asynchronous calls prolonging transaction until completion of all of them. An asynchronous call may spawn other asynchronous calls, which prolong transaction execution until the whole chain of asynchronous operations is fully completed.

Cache

Cache is a computed value having an associated function that is automatically called to renew the cache in response to state changes. Each cache function is instrumented with hooks to transparently subscribe it to those state object properties and other caches, which it uses during execution.

class MyView extends React.Component<MyModel> {
  @cache(Renew.OnDemand)
  render() {
    const m: MyModel = this.props; // just a shortcut
    return (
      <div>
        <h1>{m.url}</h1>
        <div>{m.content}</div>
      </div>
    );
  } // render is subscribed to m.url and m.content

  @cache(Renew.Immediately)
  trigger(): void {
    if (this.render.reactronic.isInvalidated)
      this.setState({}); // ask React to re-render
  } // trigger is subscribed to render
}

In the example above, the cache of the trigger function is transparently subscribed to the cache of the render function. In turn, the render function is subscribed to the url and content properties of a corresponding MyModel object. Once url or content values are changed, the render cache becomes invalidated and causes cascade invalidation of the trigger cache. The trigger cache is marked for immediate renewal, thus its function is immediately called by Reactronic to renew the cache. While executed, the trigger function enqueues re-rendering request to React, which calls render function and it renews its cache marked for on-demand renew.

In general case, cache is automatically and immediately invalidated when changes are made in those state object properties that were used by its function. And once invalidated, the function is automatically executed again to renew it, either immediately or on demand.

Reactronic takes full care of tracking dependencies between all the state objects and caches (observers and observables). With Reactronic, you no longer need to create data change events in one set of objects, subscribe to these events in other objects, and manually maintain switching from the previous state to a new one.

Cache Latency

Each cache has latency - a delay between cache invalidation and invocation of the caching function to renew the cache. Possible values are:

  • (ms) - delay in milliseconds;
  • Renew.Immediately - renew immediately with zero latency;
  • Renew.OnDemand - renew on access if cache has been invalidated;
  • Renew.Manually - manual renew (explicit only);
  • Renew.DoesNotCache - renew on every call of the function.

Try It Yourself

Feel free to try it yourself or contribute at GitHub.

Or try it as an NPM package: npm i reactronic.