I wanted to improve React

Igor Sukharev - May 5 '23 - - Dev Community

Intro

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.

What is good about React

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.

What is wrong with React

Component creation

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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.

Updating components

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.

Hooks

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]
);
Enter fullscreen mode Exit fullscreen mode

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.

In conclusion

  • 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.

How to Improve

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.

Component Constructor

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;
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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.

Using the component

Now let's attach this component to the DOM tree:

document.body.append(
  CounterButton().element,
);
Enter fullscreen mode Exit fullscreen mode

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.

Result

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 and useState hook serve this purpose. However, it is less flexible and more resource-intensive. For example, calling setCount(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

What it is

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.

Economy

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')
);
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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'}.

Lifecycle

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
);
Enter fullscreen mode Exit fullscreen mode

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

Customization

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.

About JSX

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.

Links

Full-fledged applications and other resources:

Conclusion

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
Enter fullscreen mode Exit fullscreen mode

PS: Thank you to everyone who made it to the end! 🤗 ❤️

Fusor vs React

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
. . . .