How to supercharge JavaScript with ActiveJS
Ankit Singh
Posted on December 14, 2020
If you're not happy with current state of state-management,
you're not alone; most mainstream state-management solutions are unnecessarily complex and excessively verbose.
In pursuit of an alternative, I spent 10 months building and re-building a state-management solution that doesn't suck the life out of you. It's called ActiveJS.
Succinctness is the first good thing about ActiveJS, as is apparent in this implementation of a simple "counter" in Redux vs ActiveJS.
This is how it compares with NgRx.
Only relevant LOC are included below. (excluding Angular code)
If you already feel that it's worth the investment, feel free to jump ahead to learn more about ActiveJS, otherwise, let's take a look at the problem first.
THE PROBLEM
In recent years, Angular, React, Vue, and other similar technologies have made Frontend development so much more versatile and efficient. But at the same time, state-management doesn't seem to be getting any easier.
For efficient state-management, we need a few things
- data structures that are type-safe
- data structures that can emit events on mutation
- data structures that can guarantee immutability
- data structures that can be persisted through sessions
But JavaScript has none of it; and that's a problem.
If JavaScript itself had built-in features like Observables to deal with complexity of modern day state-management we wouldn't be depending on these libraries that try to solve these basic problems in very unique yet sometimes very counterintuitive ways.
Most mainstream state-management libraries are either too verbose or only solve one problem and leave out the other. In order to build a complete solution, we have to fill the gaps with more helper-libraries. On top of managing the state, we have to manage these extra dependencies and understand their multitudes of obscure concepts, write ever more verbose code spread across multiple files that become increasingly more complex, to the point where it starts to hinder a developer's performance because the human brain has a limited cache just like a computer, called working memory, but unlike computers, we can't just scale up our brains.
THE SOLUTION
A pragmatic, reactive state management solution for JavaScript apps.
ActiveJS helps you manage the state with minimum code and effort.
The best part of ActiveJS:
Reactive Storage Units
The missing data structures that JavaScript doesn't have.
A Reactive Storage Unit, or simply called Unit, is a reactive data structure, that is
- observable
- type-safe
- cache-enabled
- optionally immutable
- optionally persistent
Built on top of RxJS Observable, in the image of JavaScript's native data structures.
Side-Note: If you're not familiar with RxJS, it's okay. To understand Units you only have to know what an Observable is. Basically, an Observable is nothing but a callback mechanism, it allows a callback function to keep receiving data over time instead of receiving it just once. Like a DOM event listener.
All the essential features required for modern state-management packed in a single package. Written in TypeScript, and strongly-typed.
Units emulate JavaScript's native data structures. There's a specialized type of Unit for each of the most used native data structures.
For example, a NumUnit is a number
counterpart that stores and provides a number
value at all times.
Let's implement a counter to understand how Units work.
We'd use a NumUnit for the counter since we expect the value to always be a number
.
▶ Initialization
// initialize a NumUnit.
const counterUnit = new NumUnit({initialValue: 6});
// NumUnit has default initial value 0,
// providing an initial value is optional.
💫 Reactive value access
// observe the Unit for current and future values
counterUnit.subscribe(value => console.log(value))
// logs 6 immediately and will log futue values
📑 Static value access
// directly access the current value
console.log(counterUnit.value()); // logs 6
📡 Reactive mutation, with built-in sanity checks
// define two pure functions that produce a new value
const increment = value => value + 1;
const decrement = value => value - 1;
// now we'll use the above pure functions as value-producers,
// the dispatch method expects a value or a value-producer-function
counterUnit.dispatch(increment); // makes the value 7
counterUnit.dispatch(decrement); // makes the value 6 again
// or just directly pass the value
counterUnit.dispatch(7); // makes the value 7
// try an invalid value
counterUnit.dispatch('20'); // NumUnit will ignore this
// NumUnit accepts nothing but numbers, not even NaN
// so the value is still 7
// Units can also be configured to prevent duplicate value dispatch.
// Had we passed the configuration flag {distinctDispatch: true}
// as in "new NumUnit({distinctDispatch: true})"
counterUnit.dispatch(7); // NumUnit would've ignored this
// because the value is already 7
This is the most basic usage of a Unit. One apparent advantage of using a Unit is that it maintains it's designated data type, to save you the need for an extra if-else
check.
Just like a NumUnit, there are 6 types of aptly named Units in ActiveJS:
BoolUnit is a
boolean
counterpart, it ensures a boolean value at all times.NumUnit is a
number
counterpart, it ensures a number value at all times.StringUnit is a
string
counterpart, it ensures a string value at all times.ListUnit is an
array
counterpart, it ensures an array value at all times.DictUnit is loosely based on
Map
, it ensures a simpleobject
value at all times.GenericUnit doesn't pertain to any specific data type, it's generic in nature, it can store any type of value.
Now that we're all caught up with the basics of a Unit, let's see what else a Unit can do.
🔙 Caching and cache-navigation
ActiveJS Units are cache-enabled, and by default, every Unit caches two values, configurable up to Infinity. When you navigate through the cache, the cache stays intact, while the value changes. This makes it very easy to travel back in time and then go back to the future.
// create a Unit
const unit = new NumUnit({initialValue: 1});
// currently the cache-list looks like this: [1]
// dispatch a value
unit.dispatch(5);
// now value is 5 and the cache-list is [1, 5]
// dispatch another value
unit.dispatch(10);
// now value is 10 and the cache-list is [5, 10]
// go back to the previous value
unit.goBack(); // now value is 5 (cache isn't affected)
// go forward to the next value
unit.goForward(); // now value is 10 (cache isn't affected)
TiMe-TrAvEl 🏎⌚ is possible!
↺ 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)
To demonstrate the next feature 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.
💎 Immutable Unit
// 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.
⚓ Persistent Unit
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, the 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 refresh the window, or open a new tab,
// on second initialization the Unit will restore its value from localStorage
// after window refresh
console.log(persitentUnit.value()); // logs 'Neo'
If you're still here, you'll not be disappointed, there's more.
🔁 Replay and Replay-ness
Every Unit immediately provides 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
🔂 Manual Replay
Imagine an Observable 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();
🔇 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 this:
// 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
// this will still work
unit.dispatch('Hello'); // but no subscriber will get triggered
// but if you check the value, there will be an unanswered Hello
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();
📅 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 unit = new ListUnit();
// subscribe to events
unit.events$.subscribe(event => console.log(event));
There's an event for almost every operation that can be performed on a Unit, for example:
// a successful dispatch
unit.dispatch([69]); // will emit EventUnitDispatch
// an invalid dispatch
unit.dispatch({}); // will emit EventUnitDispatchFail
// on freeze
unit.freeze(); // will emit EventUnitFreeze
// on ListUnit specific methods
unit.push("Hard"); // will emit EventListUnitPush with value "Hard"
// another example
unit.pop(); // will emit EventListUnitPop
You get the picture, there's an event for everything.
🛠 Treating Units like native data structures
Every Unit implements Object.prototype
methods like toString()
and redirects them to the actual stored value, and additionally, they also implement their counterparts prototype methods like NumUnit implements Number.prototype
methods to make it easier to work with the stored value. Let's see what that means.
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
num + 'XX' // '42070XX'
numUnit + 'XX' // '42070XX'
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}) // '{"num":42069, "arr": ["👽", "👻"]}'
In most cases you can treat a Unit just like a native data structure, barring a few exceptions like ListUnit and DictUnit don't have index-based property access and assignment, they use get
and set
methods instead.
Now that we know what Units are capable of individually, let's take a look at what they can do together.
One way to combine two or more Units together is to use an RxJS combination operator since all Units are Observables, it'd work the same.
But if you use RxJS operators, you'd lose access to all the other aspects of Units, and only the Observable part will remain.
That's why ActiveJS provides Cluster.
🗃 Cluster
A Cluster provides three things,
- an Observable of the combined values of its items
- static access to the combined values of its items
- direct access to its items
Let's see what that means.
// create a few Units to combine
const numUnit = new NumUnit(); // with default value 0
const strUnit = new StringUnit(); // with default value ''
const listUnit = new ListUnit(); // with default value []
// create a Cluster
const myPrecious = new Cluster({numUnit, strUnit, listUnit})
// using shorthand notation
// static value access
console.log(myPrecious.value())
// and reactive value access, emits whenever a memeber emits
myPrecious.subscribe(value => console.log(value));
// both will immediately log the following
{
numUnit: 0, strUnit: '', listUnit: []
}
// accessing the Unit through the Cluster
console.log(myPrecious.items.numUnit.value()); // logs 0
// similarly
myPrecious.items.numUnit === numUnit // true
myPrecious.items.strUnit === strUnit // true
myPrecious.items.listUnit === listUnit // true
Using Clusters you can create what you'd call a "Store" in other state managers. But instead of top-down, it's bottom-up.
Clusters can become part of other Clusters too.
// create a few Units
const boolUnit = new BoolUnit(); // with default value false
const dictUnit = new DictUnit(); // with default value {}
// create a Cluster
const myPreciousCombined = new Cluster({boolUnit, dictUnit, myPrecious});
// using shorthand notation
console.log(myPreciousCombined.value());
// logs
{
boolUnit: false,
dictUnit: {},
myPrecious: {
numUnit: 0, strUnit: '', listUnit: []
}
}
// access the Cluster though Cluster
console.log(myPreciousCombined.items.myPrecious.value());
// logs
{
numUnit: 0, strUnit: '', listUnit: []
}
If you're still here, hope that I haven't bored you to death.
Please take this refreshment before we continue to discover more awesome things you can do with ActiveJS.
Let's continue...
Probably the most repetitive thing we as Frontend developers do is make REST API calls, track their status, and share the result.
Without a state-manager, it works fine if we only have a few API calls, or don't need to share the results of API calls with any other part of our app. But as soon as we start to reach a point where sharing becomes more work than actually making the API calls, we need some sort of state-manager.
And as it currently stands, most state-managers either don't have a built-in mechanism to do this very efficiently or do it in a very verbose and repetitive way.
Enters AsyncSystem.
⏳ AsyncSystem
A few paragraphs above, we just talked about how to combine Units together using a Cluster. Another possible way to combine Units together is to make them react to each other and build some sort of a system to achieve a specific task. And what better use-case for such a system other than API calls.
An AsyncSystem is a type of System that helps in streamlining asynchronous tasks like REST API calls.
AsyncSystem uses three GenericUnits for three aspects of an async task query, response, and error, namely queryUnit
, dataUnit
, and errorUnit
, respectively; and a BoolUnit for the fourth and last aspect pending-status, named pendingUnit
.
Now let's see how to use an AsyncSystem.
▶ Initialization
// create an AsyncSystem
const userSystem = new AsyncSystem();
// it automatically create the Units and establishes relationships among them
// extract all the four Units for ease of access
const {queryUnit, dataUnit, errorUnit, pendingUnit} = this.userSystem;
// using destructuring assignment syntax
➰ Setup a stream
async function fetchAndShareData(query) {
try {
// fetch data using fetch API
const response = await fetch('https://xyz.com/u/' + query.userId);
// and extract the JSON data
const data = await response.json();
// dispatch data to the dataUnit, it also toggles the pendingUnit's state
dataUnit.dispatch(data);
} catch (err) {
// dispatch error to errorUnit, it also toggles the pendingUnit's state
errorUnit.dispatch(err);
}
}
// setup the stream by observing query values
queryUnit.subscribe(query => fetchAndShareData(query));
👂 Listening for values
Our setup is complete, we can share the appropriate Units with any part of our app now, whenever there's a change the subscriber will be notified.
// listen for queries
queryUnit.subscribe(query => console.log(query));
// listen for data
dataUnit.subscribe(data => console.log(data));
// listen for errors
errorUnit.subscribe(error => console.log(error));
// listen for pending state
pendingUnit.subscribe(isPending => console.log(isPending));
👋 Triggering new requests
We can trigger new requests from anywhere, using the queryUnit
:
// dispatch a query, it'll also set pendingUnit's value to true
// the rest will be handled by the stream we just created above
queryUnit.dispatch({userId: 42069});
That's it, we just created a System to make API calls and share the state effortlessly.
There are other automatic things that an AsyncSystem can do apart from updating the value of pendingUnit
. Such as,
- it can clear the
errorUnit
's value whendataUnit
emits a value - it can clear the
dataUnit
's value whenqueryUnit
emits a value - it can freeze the
queryUnit
while thependingUnit
has atrue
value, etc.
Moreover,
- You can use the
replay()
method ofqueryUnit
to trigger the same request again. - You can manually freeze the
queryUnit
to prevent any new requests from getting triggered.
If you want to use a different kind of Unit instead of a GenericUnit for queryUnit
, dataUnit
, or errorUnit
, you can do that too, by creating your own custom AsyncSystem using the base class of AsyncSystem, AsyncSystemBase.
That's all folks.
ActiveJS has a few more tricks up its sleeves but to keep it short it's probably better to end this post here and let you discover the rest on your own from the documentation.
Hope you find it helpful.
I'm eager to listen to your thoughts and feedback, please leave a comment or reach out to me on any other social media platform.
I'd also like to thank all the awesome people who built RxJS, NgRx, Redux, Akita, and Immutable.js for inspiration and ideas.
Peace ☮
🌏 ActiveJS Website
📖 ActiveJS Documentation
🤾♂️ ActiveJS Playground
💻 ActiveJS GitHub Repo (drop a ⭐ maybe :)
Posted on December 14, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.