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.
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 accessconsole.log(unit1.value()); // logs 0// reactive accessunit1.subscribe(value =>console.log(value)) // logs 0, will log future values// reactive non-replay accessunit1.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.
// initializeconstnumUnit=newNumUnit(); // value is 0// dispatch a new valuenumUnit.dispatch(42); // returns true for successful dispatch// value is 42 now// using dispatch method with a value-producer functionnumUnit.dispatch(currentValue => currentValue +1);// value is 43 now// invalid dispatchnumUnit.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 2constunit=newStringUnit({initialValue:'a'}); // cache is ['a']unit.goBack(); // won't work, returns falseunit.goForward(); // won't work, returns falseunit.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 valuesconsole.log(unit.cachedValues()) // logs ['a', 'b']// or check the current cache indexconsole.log(unit.cacheIndex) // logs 1// or check the count of cached valuesconsole.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 Unitconstunit=newNumUnit({initialValue:69});// clear the valueunit.clearValue(); // now value is 0 (the default value for NumUnit)// reset the valueunit.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.constrandomList=newListUnit({immutable:true});// ListUnit has default initial value []// subscribe for the valuerandomList.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
constanItem= {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 referenceanItem.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 approachconstextractedValue=randomList.value(); // get the current valueconsole.log(listValue); // logs [{type: 'city', name: 'Delhi'}]// try to mutate the extractedValueextractedValue[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.
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.
// initializeconstpersitentUnit=newStringUnit({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 persistencepersitentUnit.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 refreshconsole.log(persitentUnit.value()); // logs 'Neo'
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.
constunit=NumUnit(); // NumUnit has default initialValue 0// normal subscriptionunit.subscribe(v =>console.log(v)) // immediately logs 0// future only subscriptionunit.future$.subscribe(v =>console.log(v)) // doesn't log anything// both will log any future valuesunit.dispatch(42); // you'll see two 42 logs in the console
You can also turn the default replay-ness off.
constunit=NumUnit({replay:false});// now default Observable and future$ Observable are the same// normal subscriptionunit.subscribe(v =>console.log(v)) // doesn't log anything// future only subscriptionunit.future$.subscribe(v =>console.log(v)) // doesn't log anything// both will log any future valuesunit.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.
constunit=StringUnit({initialValue:'Alpha'});unit.subscribe(v =>/*make API request*/); // send every value to the serverunit.dispatch('Sierra'); // send another value// to emit the same value again, all you have to do isunit.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 Unitconstunit=DictUnit(); // a DictUnit has default value {}// freeze the Unitunit.freeze();// this will be ignoredunit.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 againunit.unfreeze();
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 Unitconstunit=GenericUnit(); // a GenericUnit has default value undefined// it accepts all kinds of values as the name suggests// mute the Unitunit.mute();// this will workunit.subscribe(value =>console.log(value));// logs undefined immediately, but will not log any new values until unmuted// this will still workunit.dispatch('Hello'); // but no subscriber will get triggered// but if you check the value, you'll get still get the updated valueconsole.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 syncunit.unmute();
Every Unit emits an event for every operation performed on it, you can tap into these events to take some other action.
// create a UnitconstlistUnit=newListUnit();// subscribe to eventslistUnit.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 dispatchlistUnit.dispatch([69]); // will emit EventUnitDispatch// an invalid dispatchlistUnit.dispatch({}); // will emit EventUnitDispatchFail// on freezelistUnit.freeze(); // will emit EventUnitFreeze// on ListUnit specific methodslistUnit.push("Hard"); // will emit EventListUnitPush with value "Hard"// another examplelistUnit.pop(); // will emit EventListUnitPop// and so on...
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.
constnum=42069;constnumUnit=newNumUnit({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// 42070numUnit +1// 42070 // this doesn't change the Unit's valuenum +'XX'// '42069XX'numUnit +'XX'// '42069XX' // this doesn't change the Unit's value
constarr= ['👽','👻'];constlistUnit=newListUnit({initialValue: ['👽','👻']});arr.toString() // '👽,👻'listUnit.toString() // '👽,👻'arr.join('--') // '👽--👻'listUnit.join('--') // '👽--👻'arr.push('🤖') // mutates the same arraylistUnit.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.stringifyJSON.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
constnumUnit=newNumUnit({initialValue:3});// current value is 3numUnit +1// 4// is same as thisnumUnit.value() +1// 4numUnit +'idiots'// '3idiots'// is same as thisnumUnit.value() +'idiots'// '3idiots'JSON.stringify({n: numUnit}) // {n: 3}// is same as thisJSON.stringify({n:numUnit.value()}) // {n: 3}// that's pretty much it, // in other situations the Unit will behave like an object, as it shouldnumUnit ===3// falsetypeof numUnit ==='object'// truenumUnit++// won't work, it will try to replace numUnit const with number 4// similarlyconstboolUnit=newBoolUnit({initialValue:false});boolUnit +''// 'false'boolUnit +1// 1, because false is converted to 0boolUnit.dispatch(true);boolUnit +1// 2, because true is converted to 1// similarlyconststrUnit=newStringUnit({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
✅
❌
✅
❌
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.