💾Units

Units are the most important part of ActiveJS, they are responsible for holding the state of your single page application. Units can also be used independently, without any further knowledge of ActiveJS.

Units are specialized, reactive data structures based on JavaScript's native data structures like boolean, string or array.

For example, ListUnit is an elaborate array like data structure which is also an Observable and implements all the Array.prototype methods in a way that, any mutation caused by these methods, emits a new array with the mutations applied to it.

The actual value is stored inside the Unit and can be accessed via the value() method.

All the available Units are BoolUnit, NumUnit, StringUnit, ListUnit, DictUnit, and GenericUnit.

▶ Initializing a Unit

A Unit has several configuration options, including an optional initial-value.

const unit1 = new NumUnit();
// OR
const unit2 = new NumUnit({initialValue: 0});
// OR
const unit3 = new NumUnit({initialValue: 5, cacheSize: 10});

Every type of Unit has a designated default value. NumUnit has 0 as the default value, and StringUnit has ''. That's why in the above example the unit1 and unit2 are equivalent.

📑 Accessing the value

// static access
console.log(unit1.value()); // logs 0

// reactive access
unit1.subscribe(value => console.log(value)) // logs 0, will log future values

// reactive non-replay access
unit1.future$.subscribe(value => console.log(value)) // will only log future values

As you might've noticed, when subscribing to the unit1, a value immediately gets logged to the console even though we haven't dispatched any value yet. If you are only interested in future values you can use future$ Observable, which doesn't replay the last value on subscription, it only emits future values.

📡 Dispatching a value

All Units ignore any invalid value dispatch, Units only accept their designated value type, except the GenericUnit, because it's designed to accept and store all types of values.

// initialize
const numUnit = new NumUnit(); // value is 0

// dispatch a new value
numUnit.dispatch(42); // returns true for successful dispatch
// value is 42 now

// using dispatch method with a value-producer function
numUnit.dispatch(currentValue => currentValue + 1);
// value is 43 now

// invalid dispatch
numUnit.dispatch('a string'); // this will be ignored, returns false
// value is still 43

There are two ways to dispatch a new value. You can pass a value or a value-producer-function to generate a new value to the dispatch method, the dispatch method returns true or false depending on the success of dispatch, which you can use to make any further decisions.

🔙 Using the Cached values

All Units cache 2 values by default, it allows us to go back and forth through these cached-values in such a way that it doesn't affect the cached-values, only the current-value changes. The caching mechanism works almost exactly like browser history.

The cache can be utilized in many ways, but probably the most common use case would be undo and redo capabilities. For undo feature we can use goBack method and for redo we can use goForward method respectively.

To dive into more details, see Caching to understand how it works.

// create a Unit with default cache-size of 2
const unit = new StringUnit({initialValue: 'a'}); 

// cache is ['a']

unit.goBack(); // won't work, returns false
unit.goForward(); // won't work, returns false

unit.dispatch('b'); // cache becomes ['a', 'b']
console.log(unit.value()) // logs 'b'

unit.goBack(); // cache is still ['a', 'b']
console.log(unit.value()) // logs 'a'

unit.goForward(); // cache is still ['a', 'b']
console.log(unit.value()) // logs 'b'

// we can also access all the cached values
console.log(unit.cachedValues()) // logs ['a', 'b']

// or check the current cache index
console.log(unit.cacheIndex) // logs 1

// or check the count of cached values
console.log(unit.cachedValuesCount) // logs 2

↺ Clear & Reset

Resetting a Unit to it's initial-value is as easy as calling a method. Similarly clearing the value is also that easy.

// create a Unit
const unit = new NumUnit({initialValue: 69});

// clear the value
unit.clearValue(); // now value is 0 (the default value for NumUnit)

// reset the value
unit.resetValue(); // now value is 69 again (the initial-value)

💎 Immutability

To demonstrate immutability we'd need a different kind of Unit because the NumUnit deals with a primitive type number which is already immutable.

Let's take a ListUnit to create a reactive, array like data structure.

// initialize a immutable ListUnit.
const randomList = new ListUnit({immutable: true});
// ListUnit has default initial value []

// subscribe for the value
randomList.subscribe(value => console.log(value));
// logs [] immediately and will log future values

We just created an immutable Unit, that's all it takes, a configuration flag.

✔ Mutation check

const anItem = {type: 'city', name: 'Delhi'};
randomList.push(anItem);
// this push is reactive, it'll make the Unit emit a new value

// let's try mutation by reference
anItem.type = 'state'; // this would work
// but the value of the randomList won't be affected, because every time
// a value is provided to an immutable list,
// it's cloned before storing.

// let's try another approach
const extractedValue = randomList.value(); // get the current value
console.log(listValue); // logs [{type: 'city', name: 'Delhi'}]
// try to mutate the extractedValue
extractedValue[1] = 'let me in...'; // this would work
// but the value of the randomList won't be affected, because every time
// an immutable list provides a value, it's cloned,
// to destroy all references to the stored value.

See Immutability guide for more details.

⚓ Persistence

To make a Unit persistent, all we need is a unique id so that the Unit can identify itself in the localStorage, and a configuration flag.

// initialize
const persitentUnit = new StringUnit({id: 'userName', persistent: true});
// StringUnit has default inital value ''

That's it, this StringUnit is persistent, it already saved its default value to localStorage.

✔ Persistence check

// let's dispatch a new value different than the default value to 
// properly test the persistence
persitentUnit.dispatch('Neo');
console.log(persitentUnit.value()); // logs 'Neo'

// now if we re-initialize the same Unit
// on second initialization the Unit will restore its value from localStorage
// e.g: after a window refresh
console.log(persitentUnit.value()); // logs 'Neo'

See Persistence guide for more details.

🔁 Replay and Replay-ness

Every Unit immediately provides the value when subscribed, by default, but maybe you only want the future values. For that purpose, every Unit has a built-in alternative Observable that doesn't emit immediately on subscription.

const unit = NumUnit(); // NumUnit has default initialValue 0

// normal subscription
unit.subscribe(v => console.log(v)) // immediately logs 0

// future only subscription
unit.future$.subscribe(v => console.log(v)) // doesn't log anything

// both will log any future values
unit.dispatch(42); // you'll see two 42 logs in the console

You can also turn the default replay-ness off.

const unit = NumUnit({replay: false});
// now default Observable and future$ Observable are the same

// normal subscription
unit.subscribe(v => console.log(v)) // doesn't log anything

// future only subscription
unit.future$.subscribe(v => console.log(v)) // doesn't log anything

// both will log any future values
unit.dispatch(42); // you'll see two 42 logs in the console

By default, every Unit behaves like a BehaviorSubject, but unlike a BehaviorSubject Units don't require an explicit default value, since a Unit already has a default value.

🔂 Manual Replay

Imagine a Unit is being used as a source for an API request, and you have a "refresh" button to trigger the request again. For this and many other scenarios, Units provide a manual replay method.

const unit = StringUnit({initialValue: 'Alpha'});

unit.subscribe(v => /*make API request*/); // send every value to the server

unit.dispatch('Sierra'); // send another value

// to emit the same value again, all you have to do is
unit.replay();
// all subscribers will get the same value again, in this case, 'Sierra'
// so the server should receive 'Alpha', 'Sierra', 'Sierra'

❄ Freezing

If you want a Unit to stop accepting new values, in scenarios where the state is not supposed to change. All you need to do is this:

// create a Unit
const unit = DictUnit(); // a DictUnit has default value {}

// freeze the Unit
unit.freeze();

// this will be ignored
unit.dispatch({'nein': 'nein nein'})
// so will any other mutative, or cache-navigation methods
// like goBack(), goForward(), clearValue(), resetValue() etc.

// unfreeze the Unit, and everything will start working again
unit.unfreeze();

See Freezing guide for more details.

🔇 Muting

If you want a Unit to stop emitting new values, but keep accepting new values, in scenarios where you aren't interested in new values but still don't want to lose them. All you need to do is mute the Unit.

// create a Unit
const unit = GenericUnit(); // a GenericUnit has default value undefined
// it accepts all kinds of values as the name suggests

// mute the Unit
unit.mute();

// this will work
unit.subscribe(value => console.log(value));
// logs undefined immediately, but will not log any new values until unmuted

// this will still work
unit.dispatch('Hello'); // but no subscriber will get triggered

// but if you check the value, you'll get still get the updated value
console.log(unit.value()); // logs 'Hello'

// unmute the Unit, and if the value changed while the Unit was muted,
// emit it to all the subscribers, to bring them in sync
unit.unmute();

See Muting guide for more details.

📅 Events

Every Unit emits an event for every operation performed on it, you can tap into these events to take some other action.

// create a Unit
const listUnit = new ListUnit();

// subscribe to events
listUnit.events$.subscribe(event => console.log(event));

There's an event for almost every operation that can be performed on a Unit, for example:

// a succefull dispatch
listUnit.dispatch([69]); // will emit EventUnitDispatch
// an invalid dispatch
listUnit.dispatch({}); // will emit EventUnitDispatchFail
// on freeze
listUnit.freeze(); // will emit EventUnitFreeze
// on ListUnit specific methods
listUnit.push("Hard"); // will emit EventListUnitPush with value "Hard"
// another example
listUnit.pop(); // will emit EventListUnitPop
// and so on...

See Events guide for more details.

🛠 Units vs native data structures

Units can not be used as drop-in replacements for native data structures. However, in most scenarios, a Unit instance can be used directly instead of Unit.value()

Every Unit implements valueOf and toString methods. JavaScript automatically invokes these methods when encountering an object where a primitive or string value is expected, respectively. Hence NumUnit, StringUnit, and BoolUnit can be used similar to their primitive counterparts.

Additionally, Units also implement their counterparts prototype methods like NumUnit implements Number.prototype methods to make it easier to work with the stored value. Similarly, ListUnit implements all the Array.prototype methods, StringUnit implements all the String.prototype methods, and so on.

number vs NumUnit

const num = 42069;
const numUnit = new NumUnit({initialValue: 42069});

num.toString() // '42069'
numUnit.toString() // '42069'

num.toLocaleString() // '42,069' (in an 'en' locale)
numUnit.toLocaleString() // '42,069' (in an 'en' locale)

num + 1 // 42070
numUnit + 1 // 42070 // this doesn't change the Unit's value

num + 'XX' // '42069XX'
numUnit + 'XX' // '42069XX' // this doesn't change the Unit's value

array vs ListUnit

const arr = ['👽', '👻'];
const listUnit = new ListUnit({initialValue: ['👽', '👻']});

arr.toString() // '👽,👻'
listUnit.toString() // '👽,👻'

arr.join('--') // '👽--👻'
listUnit.join('--') // '👽--👻'

arr.push('🤖') // mutates the same array
listUnit.push('🤖') // this is reactive, creates and dispatches a new array

// ListUnit is also iterable
[...arr] // a shallow copy of arr ['👽', '👻']
[...listUnit] // a shallow copy of stored value ['👽', '👻']

// and every Unit works with JSON.stringify
JSON.stringify({num, arr}) // '{"num":42069, "arr": ["👽", "👻"]}'
JSON.stringify({numUnit, listUnit}) // '{"numUnit":42069, "listUnit": ["👽", "👻"]}'

There are even more cases where you can treat a Unit just like a native data structure, barring a few exceptions like ListUnit and DictUnit don't have key-based property access and assignment, they use get and set methods instead. And the fact that BoolUnit, NumUnit or StringUnit aren't actually primitives, the typeof operator will only return object.

More Examples

const numUnit = new NumUnit({initialValue: 3});
// current value is 3

numUnit + 1 // 4
// is same as this
numUnit.value() + 1 // 4

numUnit + 'idiots' // '3idiots'
// is same as this
numUnit.value() + 'idiots' // '3idiots'

JSON.stringify({n: numUnit}) // {n: 3}
// is same as this
JSON.stringify({n: numUnit.value()}) // {n: 3}


// that's pretty much it, 
// in other situations the Unit will behave like an object, as it should
numUnit === 3 // false
typeof numUnit === 'object' // true
numUnit++ // won't work, it will try to replace numUnit const with number 4

// similarly

const boolUnit = new BoolUnit({initialValue: false});

boolUnit + '' // 'false'
boolUnit + 1 // 1, because false is converted to 0
boolUnit.dispatch(true);
boolUnit + 1 // 2, because true is converted to 1

// similarly

const strUnit = new StringUnit({initialValue: 'Hello'});

strUnit + ' World' // 'Hello World'
strUnit + 1 // 'Hello1'
strUnit.dispatch('6');
parseInt(strUnit) // 6

📊 Units vs BehaviorSubject

Unit

BehaviorSubject

Replays value on subscription

Can be configured to not replay value on subscription

Allows to listen to only future values

Accepts an initial-value

Can cache more than one value

Allows cache-navigation

Validates dispatched values

Specialized for specific data structures

Can re-emit/replay last value manually

Can be frozen

Can be muted

Can be immutable

Can be persistent

Can be reset

Can be part of a Cluster

Configuration Options

The configuration options can be passed at the time of instantiation. All the configuration options are optional. You can set them per Unit or you can also set most of them globally. See Configuration for more details.

Last updated