Yet Another Frontend Framework 

Part 1: Why?

linkTLDR;




linkThe Status Quo

Look at a simple React counter:

1import React, { useState, useEffect } from 'react';

2import { render } from 'react-dom';

3

4

5function Timer() {

6 const [count, setCount] = useState(0); // --> create a state

7 useEffect(() => { // --> after each render

8 const interval = setTimeout(() => setCount(count + 1), 1000); // --> set a timeout to update the state

9 return () => clearTimeout(interval); // --> and clean the timeout as well

10 });

11

12 return <div>You have been here for {count} seconds!</div>

13}

14

15render(<Timer/>, document.body);

Try It

What happens here is:

Why this convoluted process, you might ask? Well, we wanted to be able to define the visual representation of our counter, i.e. the div element, in a simple and expressive manner:

1<div>You have been here for {count} seconds!</div>

But the variable count's value changes, so we need to track those changes and mirror it in the visual representation, hence this overhead.

In other words, count is a reactive value, i.e. its value changes in reaction to stuff (e.g. passage of time), and React needs to go through all of that trouble to be able to mask that reactivity away.


linkThe Problem

The problem with this convoluted process is sort of obvious: Too much complexity. React has to go through all of this to mask the reactivity, as evident from this extremely simple example, causing the following issues:


linkOverhead

In our example, React needs to execute all of the described process each second. The component code (i.e. the Timer() function) is being executed every second. A virtual DOM is created and compared to the actual DOM every second.

Now this is not a problem for a fair number of cases. A lot of the time, the data-flow of your frontend code is not much more complicated than a simple timer. In such situations, React is, to quote Dan Abramov, fast enough. But quite naturally, if the data-flow complexity grows, it can easily fail to catch up.


linkControl

Because of the complexity of the process, it is also pretty fragile. Which means there are a lot of stuff that you cannot do, or you would basically broke that process.

For example, run the code snippet from earlier, and you'll see React issuing this warning:

1Warning: render(): Rendering components directly into document.body is discouraged,

2since its children are often manipulated by third-party scripts and browser extensions.

3This may lead to subtle reconciliation issues. Try rendering into a container element

4created for your app.

This is simply because React needs to maintain full control over the DOM-tree it renders the components to for all of the life-cycle of the component, as otherwise it might not be able to properly compare the Virtual DOM and the actual one.

Similarly, React needs to be able to keep track of your component's states and hooks, so useState() and useEffect() functions become rather weird ones: You cannot use them inside a condition or a loop, or they might break not only the functionality of your component, but also other components that React is handling.


Note that these are natural, unavoidable compromises in exchange for masking the reactivity. So IF it was the case that directly dealing with the reactive nature of our timer was a really hard thing to do, then it would make sense to adopt such a convoluted process and deal with its cons as well.

And when React was created, that indeed was the case. But is it still?


info ABOUT OTHER FRAMEWORKS

So for this piece I picked React as a point of comparison. But the essential problems mentioned here are inherent to ALL well-known frameworks, as they are all designed around the idea of masking the reactivity.

For example, Angular avoids using a virtual DOM and is generally faster than React. Its solution is basically recalculating scope variables upon a huge number of events (e.g. it outright overrides browser's setTimeout() function to hook into it) and then applying the necessary changes accordingly.

And while that similarly works fine for simpler situations, Angular is basically trading a lot of the already limited control you would have with React for some marginal performance gain. Ironically, that strategy quickly backfires as it is much harder to optimize Angular's change detection in cases where you've got pretty complicated data-flows.


linkThe Alternative

Let's take a deeper look at the nice representation we want to achieve that causes so much trouble:

1<div>You have been here for {count} seconds!</div>

This expression is basically describing the following DOM (sub-)tree:

1DIV

2| --- TEXT:: You have been here for

3| --- VAR:: {count}

4| --- TEXT:: seconds!

If all of the child nodes of DIV were TEXT nodes (or other HTML nodes), then we could simply use the browser's own APIs to render our <div>. However, count is a variable that changes over time. How should it be handled?


React's solution basically boils down to this:


But what if instead of masking count's reactive nature, we represent it with a proper reactive entity, e.g. RxJS's Observables?

Well, if count was an Observable instead of a plain integer, we could simplify its rendering process:


With this solution, we would not need to re-run the component code everytime count's value changes. In fact, we would need to execute the component code exactly once.

Additionally, we would not need to diff the returned sub-tree with what is already rendered. We already are changing the only place in the DOM tree that needs to change (the text content of the TEXT node we created for count).


Look at this modified, hypothetical code:

1import { render } from 'yet-another-frontend-framework';

2import { BehaviorSubject } from 'rxjs';

3

4

5function Timer() {

6 const count = new BehaviorSubject(0); // --> create an Observable

7 setInetrval(() => count.next(count.value + 1), 1000); // --> emit its next value on an interval

8

9 return <div>You were here for {count} seconds!</div>;

10}

11

12render(<Timer/>, document.body);

Try the Actual Version

Now our rendering process becomes:


Quite simple, isn't it?

This simple process is not just potentially much faster (less calculations), it is also much more stable and flexible due to its inherent simplicity. There is no virtual DOM, and the component is rendered exactly once, so you can easily mess with all parts of the DOM without breaking anything, which in turn brings high levels of integratibility for your components.

Additionally, as the component code is only executed once, there is no need to keep track of any state or hook, so you do not have the weird and easy-to-break machinery of React hooks. I mean, you can even bring the whole process out of the scope of any component and gain the same result:

1import { render } from 'yet-another-frontend-framework';

2import { BehaviorSubject } from 'rxjs';

3

4

5const count = new BehaviorSubject(0); // --> create an Observable

6setInetrval(() => count.next(count.value + 1), 1000); // --> emit its next value on an interval

7render(<div>You were here for {count} seconds!</div>, document.body);


To top it off, lets even use RxJS's own timer() function which directly gives us an Observable emitting values each second:

1import { render } from 'yet-another-frontend-framework';

2import { timer } from 'rxjs';

3

4

5render(<div>You have been here for {timer(0, 1000)} seconds!</div>, document.body);

Try the Actual Version

linkRecap and The Journey Ahead ...

So to recap:

So with this reasoning, I embarked on creating yet another frontend framework (or library). You can checkout the result here. The library itself is pretty young, and in dire need of testing, benchmarking and optimization. But it has already helped me a lot with my frontend efforts (for example it powers these blogs and all oss documentation that I do through CODEDOC).

If you enjoyed this piece, stay tuned for upcoming parts where we'll go through the story of how it was designed and created!



Hero Image by Wendy Scofield from Unsplash.

Hero Image by Hafidh Satyanto from Unsplash.

TLDR;The Status QuoThe ProblemOverheadControlThe AlternativeRecap and The Journey Ahead ...

Home

On Reactive Programmingchevron_right
Yet Another Frontend Frameworkchevron_right
Other Articleschevron_right