I have been coding for a long time, and I have been using React for over five years.
During this time, I have had several ideas on how React could be improved.
About three years ago, I started working on implementing these ideas. First, I tested the concepts, and then decided to turn everything into a library.
In this article, I would like to tell you about what came out of it.
Firstly, this is the approach of React in combining Javascript and HTML in one code. Other frameworks did not manage to do this as well. For example, some frameworks invented their own template programming languages, which duplicate the constructions of the JavaScript language, such as if
, else
, each
...
In my opinion, this is an unnecessary cognitive load. Fortunately, some frameworks have since acquired support for JSX.
Another important but not so obvious feature of React is the one-way data flow and the corresponding mental model, which helps to structure the code while remaining flexible.
In React, it is recommended to use functional components instead of class-based ones. This certainly makes the work easier.
Let's consider the following example:
function CounterButton() {
const [count, setCount] = useState(0);
const handleClick = () => setCount(count + 1);
return (
<button onClick={handleClick}>
You clicked {count} times
</button>
);
}
The CounterButton
function is both a component constructor and a function that updates data. Such a mix of responsibilities is a bad practice, and it creates many problems, which we will talk about now.
React calls the CounterButton
function every time data is updated. All objects created inside this function will be recreated every time it is called.
In the example, the handleClick
function is such an object. Resources could be saved if this function were created once in the constructor. But it does not exist in functional components.
Also, the CounterButton
function returns a new virtual DOM object every time it is called.
New objects are created on the heap, and old objects are removed by the garbage collector. This process causes fragmentation and excessive memory usage. Periodic defragmentation is also required. All of these processes are resource-intensive.
It is worth noting that in our specific example, this aspect of React's behavior does not matter. But for large applications with hundreds or thousands of components, it becomes noticeable.
That is why the React team, starting with version 17, began developing concurrent mode to allow updating the UI while React processes are running.
One could argue that using hooks would partially solve the issue of creating unnecessary objects.
By the way, in one company where I worked, the policy was to always use hooks.
So here is an example:
const handleClick = useCallback(
() => setCount(count + 1),
[count]
);
On one hand, hooks allow you to forget about unnecessary updates of components located lower in the tree. But on the other hand, they do not solve the problem of creating "garbage" objects. For instance, the function () => setCount(count + 1)
is created only to be discarded by the hook if count
has not changed. Moreover, a new array object [count]
is also created.
The same thing happens with other hooks as well. For example, compare constructor(){code();}
and useEffect(() => {code();}, [])
. In the first case, the code will run only once in the constructor. In the second case, two extra objects will be created on each update.
Also, the hooks mechanism itself is additional logic that affects performance. But the biggest drawback of hooks, in my opinion, is their verbosity. It is incredibly boring to write wrappers for simple operations. And the readability of the code suffers as well.
- Many unnecessary objects are created and deleted on the heap, causing fragmentation, memory overconsumption, and defragmentation, which deteriorates performance.
- More logic is introduced for the concurrent mode, which also worsens performance.
- Additional logic of the use of hooks also worsens performance.
- Hooks make the code more verbose and harder to understand.
Next, I would like to propose solutions to the aforementioned problems.
So, in short, we need to:
- Improve performance.
- Reduce verbosity.
- Make the code more explicit and easy to understand.
Let's start with a hypothetical example:
function CounterButton() {
let count = 0;
const handleClick = () => {
count++;
btn.update();
};
const btn = button(
{ click$e: handleClick }, // props
() => `You clicked ${count} times`, // child
);
return btn;
}
It looks very similar to a React example. For simplicity, let's omit JSX for now.
The only unknown factor here is the button
function, where all the "magic" happens. We can imagine how it might work based on our example.
The button
function should perform the following actions:
- Create a DOM object
HTMLButtonElement
. - Set a click event handler
handleClick
for the button. - Set the text of the button using the value returned by the lambda function
() => `You clicked ${count} times`
. - Allow updating the text of the button using the result of the lambda function.
It makes sense that the button
function should create and return an object with two properties: element
and update
.
The class for this object could look like this:
class Component {
get element() {}; // return DOM Element object
update() {}; // update dynamic data
}
When the button is clicked, the counter is increased by one count++
. Then the btn.update()
method is called, which executes the lambda function and updates the button text.
Now let's attach this component to the DOM tree:
document.body.append(
CounterButton().element,
);
First, we call the CounterButton
function, which creates and returns the component, and then attach its element to the DOM tree.
Now, the button with a counter should be displayed and correctly count the number of clicks.
Okay, let's suppose that:
- In addition to
button
, there is a full set of HTML functions:h1
,div
,span
, and so on... - Components created by these functions can contain child components, and so on to infinity.
- When the parent component is updated, its child components will be updated as well.
That's it! 🤗
This approach solves all the aforementioned problems and preserves the good features of React.
I bet you expected something bigger.
Do not worry that component updates are made explicitly. Mostly, updates are triggered by higher-order components. However, it is also possible to update any part selectively.
By the way, in React, explicit updates also need to be called. The
setState
function anduseState
hook serve this purpose. However, it is less flexible and more resource-intensive. For example, callingsetCount(count + 1)
will set the variable through the state mechanism, and then add the need for an update to the queue through the update mechanism. As you can see, there is again a mixing of responsibilities.
Thus, the aforementioned concept helps to solve problems in the following way:
- Firstly, the component creation function serves as its constructor and is called only once. Accordingly, objects created inside the function are also created only once and do not require such hacks as hooks.
- There are no hooks. There is no additional logic. Everything happens explicitly and readably.
- Concurrent mode is not necessary, as firstly, the amount of logic has significantly decreased, and secondly, we have complete control over the processes of creation and update, and can easily insert interface rendering where needed.
Fusor is a simple library that helps declaratively create and update DOM elements.
In Fusor, there are no additional mechanisms for:
- Props
- State
- Context
- Lifecycle
Fusor is a minimalistic and transparent approach that uses the constructs of the Javascript language and DOM functions "almost without a library".
Nevertheless, Fusor can fully replace React! How is that possible? Let's take a closer look.
Fusor is an economical library.
Fusor does not create a heap of unnecessary objects on the heap.
For example, let's consider the following code:
import { div, p } from '@fusorjs/dom/html';
const wrapper = div(
p('I am the static text')
);
The variable wrapper
will contain an HTMLDivElement
object, not a Component
as in the example with the counter button, since there are no dynamic parts here.
If we take the example with the button and modify it slightly:
import { button } from '@fusorjs/dom/html';
function CounterButton() {
let count = 0;
const handleClick = () => {
count++;
btn.update();
};
const btn = button(
// props:
{ click$e: handleClick },
// child text nodes:
'You clicked ', // static
() => count, // dynamic
' times', // static
);
return btn;
}
So you can see that now only one of the three child elements of the button is dynamic. And the btn
variable will be an object of the Component
class.
When updating, only the value of one text node, to which the lambda function () => count
is bound, will be changed if the value is different from what is already there.
Thus, an additional component object is only created if it contains dynamic data.
Dynamic data can also be in properties. For example, {class: () => selected ? 'selected' : 'unselected'}
.
Component lifecycle is the only mechanism that Fusor lacks to be able to replace React.
Since Fusor does one thing and does it well, it does not have a component lifecycle logic. However, such logic exists in native custom elements.
Fusor fully supports all web standards, including web components. Therefore, they can be used to connect lifecycle events.
Nevertheless, for convenience, Fusor has a custom element called fusor-life
and its wrapper component Life
:
import { Life } from '@fusorjs/dom/life';
const wrapper = Life(
{
connected$e: () => {},
disconnected$e: () => {},
// ... other props
},
// ... children
);
Compare this to the React component lifecycle mechanism and the O(n)
traversal of the component tree.
Fusor | fusor-life | React | |
---|---|---|---|
Mounting | constructor | connected | constructor, getDerivedStateFromProps, render, componentDidMount |
Updating | update | attributeChanged | getDerivedStateFromProps, shouldComponentUpdate, render, getSnapshotBeforeUpdate, componentDidUpdate |
Unmounting | disconnected | componentWillUnmount |
It is not necessary to use functions located in html
, svg
, or life
. They exist to avoid having to create them manually and to demonstrate how it is done.
For example, if you need to create a specific set of HTML tags, you can easily do so.
If you need to use a single function for all elements, you can use the h
function for HTML or the s
function for SVG. For example: h('div', props, children)
. Or you can make other variations.
There is also a more flexible function create(element, props, children)
. Using this function, you can configure the use of JSX with Fusor.
JSX support will also be available.
Functional notation is also good because:
- It is pure JavaScript with regular comments.
- No conversion, build, or compilation is required.
- You can use any number of props and children in any order.
Full-fledged applications and other resources:
- A counter button - an interactive example of an application.
- A tutorial with recipes (code) - an interactive application with the main usage scenarios of Fusor: lifecycle, request, routing, etc.
- Implementation of TodoMVC (code) - an application through which Fusor was developed. Therefore, it turned out not to be the easiest and most beautiful, but ideologically correct. Also, no lifecycle events are needed in this application.
- Fusor repository and documentation.
There are applications. Examples of basic usage scenarios are available. Test coverage is also available. The API has been stable for quite some time. Fusor can be used in production.
npm install @fusorjs/dom
PS: Thank you to everyone who made it to the end! 🤗 ❤️
Fusor | React | |
---|---|---|
Component constructor | Explicit, function | Combined with updater in funtion components |
Objects in Component | Created once | Re-created on each update even with memoization |
State, effects, refs | Variables and functions | Complex, hooks subsystem, verbose |
Updating components | Explicit, flexible | Implicit, complex, diffing |
DOM | Real | Virtual |
Events | Native | Synthetic |
Life-cycle | Native, custom elements | Complex, tree walking |