Я хотел улучшить React
Tags: react, javascript, solidjs, webcomponents
Published: 2023-05-01 20:49:24

## Привет

Я давно пишу код, а React использую более пяти лет.

За это время у меня возникло несколько идей о том, как можно было бы улучшить React.

К реализации этих идей я приступил около трех лет назад. Сначала проверил концепцию, потом решил оформить всё в виде библиотеки.

А о том, что из этого вышло, я бы хотел рассказать в этой статье.

## Чем хорош React

Во-первых, это подход React в объединении Javascript и HTML в одном коде. У остальных это получилось не так хорошо.

Например, для некоторых фреймворков были изобретены свои микроязыки программирования шаблонов, которые дублируют конструкции языка JavaScript, как: `if`, `else`, `each`...

По моему мнению, это лишняя когнитивная нагрузка. Благо, некоторые с тех пор обзавелись поддержкой JSX.

Другая важная, но не столь очевидная черта React - это односторонний поток данных и соответствующая ментальная модель, что помогает структурировать код, оставаясь при этом гибким.

## Что не так с React

### Создание компонента

В React рекомендуется использовать *функциональные* компоненты вместо *классовых*. Это, безусловно, облегчает работу.

Рассмотрим такой пример:

```jsx
function CounterButton() {
  const [count, setCount] = useState(0);
  const handleClick = () => setCount(count + 1);
  return (
    <button onClick={handleClick}>
      You clicked {count} times
    </button>
  );
}
```

Функция `CounterButton` является *конструктором* компонента и функцией, обновляющей данные. Такое смешение сфер ответственности - плохая практика, и это порождает множество проблем, о которых сейчас и поговорим.

### Обновление компонента

React вызывает функцию `CounterButton` каждый раз при обновлении данных. Все объекты, созданные внутри этой функции, будут создаваться заново при каждом ее вызове.

В примере функция `handleClick` является таким объектом. Можно было бы сэкономить ресурсы, если бы эта функция создавалась один раз в *конструкторе*. Но его нет в *функциональных* компонентах.

Также функция `CounterButton` возвращает новый объект виртуального DOM при каждом вызове.

> Новые объекты создаются на [куче](https://en.wikibooks.org/wiki/Memory_Management/Stacks_and_Heaps), а старые удаляются [сборщиком мусора](https://en.wikibooks.org/wiki/Memory_Management/Garbage_Collection). При этом возникает фрагментация и перерасход памяти. Также периодически должна выполняться ее [дефрагментация](https://en.wikibooks.org/wiki/Memory_Management/Memory_Compacting). Все эти процессы ресурсоёмкие.

Хочу отметить, что в нашем конкретном примере эта особенность работы React не имеет значения. Но для больших приложений, в которых используются сотни и тысячи компонентов, это становится заметным.

Поэтому команда React, начиная с версии 17, начала разработку [параллельного режима](https://17.reactjs.org/docs/concurrent-mode-intro.html), чтобы дать возможность обновлять пользовательский интерфейс во время обработки процессов React.

### Хуки

Можно было бы возразить, что использование *хуков* помогло бы частично исправить ситуацию с созданием лишних объектов.

> Кстати, в одной компании, где я работал, была политика - всегда использовать *хуки*.

Итак:

```javascript
const handleClick = useCallback(
  () => setCount(count + 1), 
  [count]
);
```

С одной стороны, *хуки* позволяют забыть об избыточных обновлениях компонентов, находящихся в дереве ниже. Но с другой стороны, они никак не решают проблемы создания "мусорных" объектов. Так, функция `() => setCount(count + 1)` создается, чтобы быть отброшенной хуком, если `count` не изменился. Более того, создается новый объект массива `[count]`.

То же самое происходит и с другими *хуками*. Например, сравните: `constructor(){code();}` и `useEffect(() => {code();}, [])`. В первом случае код выполнится один раз в конструкторе. Во втором - на каждом обновлении будут создаваться два лишних объекта.

Также сам механизм *хуков* является дополнительной логикой, которая влияет на производительность. Но самый главный недостаток *хуков*, по моему мнению, это их многословность. Как же это невероятно скучно - писать обертки для простейших операции. А еще читабельность кода страдает.

### Что в итоге

- Много лишних объектов создается и удаляется на *куче*, вызывая ее фрагментацию, перерасход памяти и дефрагментацию, что ухудшает производительность.
- Вводится больше логики для *параллельного режима*, которая ухудшает производительность.
- Дополнительная логика работы *хуков* также ухудшает производительность.
- *Хуки* делают код более многословным и сложным для понимания.

## Как улучшить

Далее, я хотел бы предложить решение вышеуказанных проблем.

Итак, если упрощенно, нам надо:

- Улучшить производительность.
- Уменьшить многословность.
- Сделать код более явным и простым для понимания.

### Конструктор компонента

Начнем с гипотетического примера:

```javascript
function CounterButton() {
  let count = 0;
  const handleClick = () => {
    count++;
    btn.update();
  };
  const btn = button(
    { click$e: handleClick }, // props
    () => `You clicked ${count} times`, // child
  );
  return btn;
}
```

Выглядит очень похоже на пример, написанный на React. Для простоты пока опустим JSX.

Единственная неизвестная составляющая здесь - это функция `button`, в которой происходит вся "магия". Можно представить, как она могла бы работать, исходя из нашего примера.

Нужно, чтобы функция `button` выполняла следующие действия:

- Создавала объект DOM `HTMLButtonElement`.
- Устанавливала для него обработчик события клика мышки `handleClick`.
- Инициировала текст кнопки возвращённым значением лямбда-функции ``` () => `You clicked ${count} times` ```.
- Предоставляла возможность обновлять текст кнопки результатом вызова лямбда-функции.

Логично, что функция `button` должна создать и вернуть объект с двумя свойствами: `element` и `update`.

Класс этого объекта мог бы выглядеть так:

```javascript
class Component {
  get element() {}; // вернуть объект DOM Element
  update() {}; // обновить динамические данные
}
```

При клике на кнопке происходит увеличение счетчика на единицу `count++`. Затем вызывается метод `btn.update()`, который выполняет лямбда-функцию и обновляет текст кнопки.

### Использование компонента

Теперь присоединим этот компонент к дереву DOM:

```javascript
document.body.append(
  CounterButton().element,
);
```

Сначала вызваем функцию `CounterButton`, которая создает и возвращает компонент, а затем присоединяем его элемент к дереву DOM.

Теперь кнопка со счетчиком должна отобразиться и корректно считать количество кликов.

Хорошо, еще предположим, что:

- Помимо `button`, есть весь набор HTML-функции: `h1`, `div`, `span`...
- Созданные этими функциями компоненты могут содержать *дочерние* компоненты и так до бесконечности.
- При обновлении *родительского* компонента будут обновлены его *дочерние* компоненты.

Всё! 🤗

Миссия выполнена.

### Результат

Готов поспорить, вы ожидали нечто большее.

Эта концепция работы с компонентами решает все вышеизложенные проблемы, а также сохраняет хорошие черты React.

Не стоит переживать, что обновления компонентов делаются явно. В основном обновления триггерят компоненты *верхнего порядка*. Но можно и точечно обновить любую часть.

> Кстати, в React также нужно вызывать обновления явно. Функция `setState` и хук `useState` служат этой цели. Только менее гибко и более ресурсозатратно. Например, вызов `setCount(count + 1)` вызовет установку переменной через механизм состояния, затем добавит в очередь необходимость обновления через механизм обновлений. Как видно, тут снова происходит смешение сфер ответственности.

Итого, вышеизложенная концепция помогает решить проблемы следующим образом:

- Во-первых, функция создания компонента является его *конструктором* и вызывается лишь один раз. Соответственно, объекты, созданные внутри функции, также создаются один раз и не нуждаются в таких *хаках*, как *хуки*.
- Нет *хуков*. Нет дополнительной логики. Все происходит наглядно и читаемо.
- *Параллельный режим* становится не нужен, так как, во-первых, логики стало значительно меньше, а во-вторых, мы полностью контролируем процессы создания и обновления и можем легко вставить прорисовку интерфейса там, где нужно.

## Fusor (Фьюзор)

### Что это такое

*Fusor - это простая библиотека, помогающая декларативно создавать и обновлять элементы DOM.*

Во Fusor нет дополнительных механизмов для:

- *Свойств*
- *Состояния*
- *Контекста*
- *Жизненного цикла*

Fusor - это максимально "легкий" и прозрачный подход "почти без библиотеки" по полной использующий конструкций самого языка Javascript и функции DOM.

Но тем не менее Fusor может полноценно заменить собой React! Как такое возможно? Давайте разбираться.

### Экономика должна быть экономной

*Fusor - это экономичная библиотека.*

Fusor не порождает кучу лишних объектов на *куче*.

Если взять такой пример:

```javascript
import { div, p } from '@fusorjs/dom/html';
const wrapper = div(
  p('I am the static text')
);
```

То переменная `wrapper` будет содержать объект `HTMLDivElement`, а не `Component`, как в примере с кнопкой-счетчиком, так как здесь нет *динамических* частей.

Если же взять пример с кнопкой и немного его изменить:

```javascript
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;
}
```

То можно увидеть, что теперь только один из трех *дочерних* элементов кнопки является *динамическим*. А переменная `btn` будет уже объектом класса `Component`.

При обновлении точечно будет изменено значение только одной текстовой *ноды*, к которой привязана лямбда-функция `() => count`, и только если значение будет отличаться от уже находящегося там.

Таким образом, дополнительный объект компонента создается только в том случае, если он содержит в себе *динамические* данные.

Также *динамические* данные могут быть в *свойствах*. Например `{class: () => selected ? 'selected' : 'unselected'}`.

### Жизненный цикл

Жизненный цикл компонентов - это единственный механизм, которого недостаёт для полноценной замены React.

Так как Fusor делает одну вещь и делает её хорошо, то в нём нет логики жизненного цикла. Зато такая логика есть в нативных [кастомных элементах](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#using_the_lifecycle_callbacks).

А во Fusor есть 100% поддержка всех вэб стандартов, в том числе и *вэб компонентов*. Поэтому можно использовать их для подключения событий жизненного цикла.

Тем не менее для удобства во Fusor добавлен *кастомный элемент* `fusor-life` и его компонент-обёртка `Life`:

```javascript
import { Life } from '@fusorjs/dom/life';
const wrapper = Life(
  {
    connected$e: () => {},
    disconnected$e: () => {},
    // ... other props
  },
  // ... children
);
```

Сравните это с механикой жизненного цикла React и обходом дерева компонентов `O(n)`.

|            | Fusor       | fusor-life       | React                                                                                                |
| ---------- | ----------- | ---------------- | ---------------------------------------------------------------------------------------------------- |
| Mounting   | constructor | connected        | constructor, getDerivedStateFromProps, render, componentDidMount                                     |
| Updating   | update      | attributeChanged | getDerivedStateFromProps, shouldComponentUpdate, render, getSnapshotBeforeUpdate, componentDidUpdate |
| Unmounting |             | disconnected     | componentWillUnmount                                                                                 |

### Кастомизация

Необязательно использовать функции, находящиеся в `html`, `svg`, или `life`. Они существуют, чтобы не пришлось создавать их самостоятельно, а также чтобы показать на их примере, как это делается.

Например, если нужно создать определенный набор HTML-тэгов, можно легко сделать это.

Если нужно использовать одну функцию для всех элементов, то можно использовать функции `h` для HTML или `s` для SVG. Например: `h('div', props, children)`. Либо написать другие вариации.

Существует и более гибкая функция `create(element, props, children)`. Используя ее, можно настроить работу JSX с Fusor.

### О JSX

Поддержка JSX будет.

Функциональная нотация тоже хороша. Потому что:

- Это чистый JavaScript с обычными комментариями.
- Не нужно преобразования, сборки или компиляции.
- Можно использовать любое количество *props* и *children* в любой последовательности.

### Ссылки

Полноценные приложения и другие ресурсы:

- [Кнопка-счетчик](https://codesandbox.io/s/fusor-intro-cvbhsk?file=/src/index.js) - интерактивный пример приложения.
- [Туториал с рецептами](https://fusorjs.github.io/tutorial/) ([код](https://github.com/fusorjs/tutorial)) - это интерактивное приложение с основными сценариями использования Fusor: жизненный цикл, запрос, роутинг...
- [Имплементация TodoMVC](https://fusorjs.github.io/todomvc/) ([код](https://github.com/fusorjs/todomvc)) - приложение с помощью которого был разработан Fusor. Поэтому оно получилось не самым простым и красивым, но зато идеологически правильным. Также в этом приложении не нужны события жизненного цикла.
- [Репозиторий Fusor и документация](https://github.com/fusorjs/dom).

## Заключение

Приложения есть. Примеры основных кейсов использования есть. Покрытие тестами тоже есть.
АПИ стабилизировался достаточно давно. Можно использовать Fusor в продакшене.

```bash
npm install @fusorjs/dom
```

PS: Спасибо всем, кто дочитал до конца! 🤗 ❤️

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