Reactronic - The Concept of Transactionally-Reactive State Management
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:
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
.