Reactivity in a framework is a declarative programming model that takes care of keeping the DOM(Document Object Model) in sync with the updates to current state
I know that it's hard to sip, let's get practical so that we solidify our mental models and get a good hold of it!
Let's code up a plain old counter by hand. With the advent of many javascript frameworks and libraries, it is a pretty easy task to accomplish. Will it be the same when developed with plain javascript?
Forget all the frameworks and libraries, Your only tool is javascript now and just get ready for the adventure!
index.html:
Our counter will be rendered into
# app
.
index.js:
class Counter {
count = 0;
handleIncrement = () => {
this.count++;
};
handleDecrement = () => {
this.count--;
};
}
I'm defining a class
Counter
, with a property
count
that defaults to
0
and two methods
handleIncrement
,
handleDecrement
that handles increment and decrement actions respectively. Our current state is the
count
property. Whenever the state is updated, our DOM should be synced up. It shouldn't be stale.
Since we are dealing with plain JS, we should be creating our increment and decrement buttons by hand right? That's what our next task is!
index.js:
setUpButton(action) {
const actionHash = {
Increment: this.handleIncrement,
Decrement: this.handleDecrement
};
const button = document.createElement("BUTTON");
button.textContent = action;
button.onclick = actionHash[action];
return button;
}
Our
setupButton
method ensures that It creates a button and associates the respective
onclick
handler according to the
action
passed as an argument. So we are done with the functionality. Not bad till now. Let's get it into
DOM
. We should be coding up our
render
method now!
index.js:
render() {
const app = document.getElementById("app");
app.innerHTML = "";
const count = document.createElement("DIV");
count.textContent = this.count;
const elementsToAppend = [
count,
this.setUpButton("Increment"),
this.setUpButton("Decrement")
];
const fragment = document.createDocumentFragment();
elementsToAppend.forEach(element => {
fragment.append(element);
});
app.appendChild(fragment);
}
This is more of a straight-forward implementation of
render
method. DOM should be kept in sync with our state
count
. So we are clearing up any stale elements that were previously rendered at first by setting
innerHTML
to an empty
string
. We are creating a
div
element that renders our
count
value. Then we set up both our increment and decrement buttons and finally we append everything to the
# app
element.
Hurray! we are done soon. Let's check whether it's working.
index.js:
new Counter().render();
***Output*** 🤯
Oops, it didn't work as expected 😱
While checking our code we can find that, once we update our state we've failed to render our app again! That's the cause. Let's fix it 🛠
index.js:
handleIncrement = () => {
this.count++;
this.render();
};
handleDecrement = () => {
this.count--;
this.render();
};
Finally 😅
The complete source code can be found here.
OMG! see how imperative our solution is 😓. What if we include a magic layer that takes care of these nitty-gritty things. That is, whenever our current state updates, our app should magically rerender declaratively. That's the way to go, right? What if we add another state in the future and failed to do the same? This solution is less maintainable and not future proof.
To the surprise, the modern javascript frameworks and libraries actually act as the magic layer underhood that takes care of these low-level tasks and makes you more productive by letting you concentrate entirely on the app business logic. The DOM will be in-sync with the state updates and that's a promise given by modern frameworks and libraries.
And also we can't simply rerender the whole
app
for a single state change. These tools also ensure that they efficiently update the
DOM
and only
re-render
the parts that are only necessary.
These tools have their own ways of handling state management.
How does React handle it?
React achieves state tracking via the
useState
API in functional components.
With
useState
, now the solution is more maintainable and readable and less error-prone. Future updates can be done seamlessly.
useState
function imported from
react
when invoked, returns an array. It contains two elements, the first one denotes the state variable itself, while the second element references a function that can be invoked to update that particular state variable. You can't simply use
this.count++
or
this.count--
directly as we do in plain JS. We should only use the respective state updater functions. This solution is more declarative than the previous one that we hand-coded with plain JS.
But what if I say that there is a more elegant way for achieving this?
Ember, a framework for ambitious web applications provides us some great APIs that are more natural-looking and syntactically very declarative. You can be free from using any state updater functions like
this.setState()
. Just
count++
or
count--
is enough. This is how we do in javascript right?
Octane edition is the latest update in
Ember
. This has amazed me with lots of cool new features and a more organized declarative programming model. If I had to pick one out of them, the new Reactivity model earns the medal, to be honest.
Let's see how our counter can be implemented with
Ember
🤗
Counter.js:
Counter.hbs:
I personally feel this approach to be more natural. You just tell
Ember
which properties you wanna keep in the state.
Ember
automatically tracks that particular property and keeps the
DOM
in-sync on updates to it. Also, your markup is now split into a separate
handlebars
file, so that your business logic now becomes less clunky and more readable 🤩
This is a lot for now. Let me know your thoughts regarding our approach in the comments below.
Interested to know more about how
@tracked
imported from
@glimmer/tracking
achieves this complex work underhood?
Want to know how
@tracked
keeps the track of different state properties and triggers rerender based on the updates on them?
Curious to know about their internals?
This is what exactly I'll be covering up in my next post. Can't wait for excitement! Meet you there again folks, bye! 🤟🏻