- PVSM.RU - https://www.pvsm.ru -

Я ненавижу React

Да, я его действительно ненавижу. Мне кажется, что команда React'а презирает разработчиков, и я презираю их в ответ. Все их решения направлены на то, чтобы сделать разработку сложнее, медленнее и непредсказуемее. На сегодняшний день они даже умудрились сломать работу JavaScript. Уму непостижимо, почему им это сходит с рук.

Рендер – это не рендер

Я знаком с React'ом с первой его версии. Тогда я фрилансил, и моим любимым стеком был ModX [1] с его шаблонами + jQuery [2]. Чуть раньше React'а появился шаблонизатор Fenom [3], и он был очень крутой. Увидев JSX, я сначала подумал: «Круто! Это же как Fenom, только в браузере». Но первое разочарование наступило сразу же. Рендер — это не рендер:

class MyComponent extends React.Component {
  count = 0;

  constructor() {
    super();
    
    setInterval(() => {
      ++this.count;
      this.render(); // Этот вызов ни к чему не приведёт!
    });
  }
  
  render() {
    return <div>Current count: {this.count}</div>;
  }
}

Ну почему?! О чём они думали? Почему вызов метода render не приводит к рендеру? Это же так очевидно! Просто представьте параллельную вселенную, где этот класс был бы реализован вот так:

class MyComponent extends Component {
  // Вызывается, когда сверху пришли новые пропсы;
  // компонент сам решает, нужно ли вызвать рендер.
  onPropsChange() {}

  // Вызывается перед удалением ноды из DOM.
  onDisconnected() {}

  // Вызывается после вставки ноды в DOM.
  onConnected() {}
  
  // Реально рендерит (обновляет DOM).
  render() {}
}

Скольких проблем попросту бы не существовало? С какой легкостью мы бы подключали внешние источники данных! Например:

// import { makeObservable, autorun } from 'kr-observable';

// Или
// import { makeObservable, autorun } from 'mobx'

// Или
import { 
  reactive as makeObservable, 
  watchEffect as autorun 
} from 'vue';

const state = makeObservable({})

class MyComponent extends React.Component {
  
  constructor() {
    super();
    this.disposer = autorun(this.render); 
  }

  onDisconnected() {
    this.disposer();
  }
  
  // Реально рендерит (обновляет DOM).
  render() {
    if (state.loading) return <div>Loading...</div>
    return <div>{state.data}</div>
  }
}

Но это не путь React'а. В Solid.js, например, мы можем это сделать одной строчкой кода: enableExternalSource, а React всячески препятствует любой интеграции. React даже контрибьютингу всячески препятствует!

Фиктивный open-source

Да, React open-source, но контрибьютеров не ждёт. Недавно мне понадобилось посмотреть внутренности хука useSyncExternalStore (о котором ниже), но мне это не удалось, потому что репозиторий React [4]'а — это лабиринт, и для навигации там нужен путеводитель. Смотрите сами.

Идём в репозиторий React'а, в пакет use-sync-external-store [5] и видим это:

import * as React from 'react';
export const useSyncExternalStore = React.useSyncExternalStore;

Это всё. Зачем целая директория для этого пакета?

Продолжаем искать. Это хук, значит, искать надо среди хуков, верно? Идём в пакет react, в файл ReactHooks.js [6] и видим это:

export function useSyncExternalStore<T>(
  subscribe: (() => void) => () => void,
  getSnapshot: () => T,
  getServerSnapshot?: () => T,
): T {
  const dispatcher = resolveDispatcher();
  return dispatcher.useSyncExternalStore(
    subscribe,
    getSnapshot,
    getServerSnapshot,
  );
}

И тут ничего. Может, resolveDispatcher поможет? Пробуем:

import ReactSharedInternals from 'shared/ReactSharedInternals';

function resolveDispatcher() {
  const dispatcher = ReactSharedInternals.H;
  return ((dispatcher: any): Dispatcher);
}

Опять нет! Но нам дали новую подсказку: shared/ReactSharedInternals [7]. Давайте посмотрим:

import * as React from 'react';

const ReactSharedInternals =
  React.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE;

export default ReactSharedInternals;

Потрясающе. Всё для людей. Образцовый open-source репозиторий!

Какова вероятность, что, обнаружив какой-то баг, я смогу попробовать его исправить и отправить PR, а не ждать и надеяться, что мейнтейнеры когда-нибудь сделают это сами? Около нулевая. Именно поэтому в репозитории React'а висят баги от 2014 года [8] — не потому, что их кто-то другой не может исправить, а потому что это фиктивный open-source. Это системная непрозрачность, и она прослеживается во всём, что делает React.

Хуки – великое разочарование

Начнём с упомянутого выше useSyncExternalStore. Казалось бы, React одумался, сдался, признал, что внешний стейт — это не грех, а реальность, и предоставил разработчикам удобный инструмент для интеграции внешнего стейта с прекрасным, лаконичным и очевидным API. Реализуем интерфейс ExternalStore:

class Store implements ExternalStore {
  subscribe(subscriber) {
    this.subscribers.add(subscriber);
    return this.unsubscribe;
  }

  unsubscribe() {}
}

// и используем
const store = new Store();

function Component() {
  useSyncExternalStore(store)
  // ...
}

Красиво, же?

При изменениях в нашем «сторе» мы вызываем переданный при подписке subscriber, и это вызывает ре-рендер. При анмаунте React вызывает метод unsubscribe. Поверили? А зря! Команда React'а ставит собственную «религию» выше удобства разработчиков, поэтому реальный интерфейс useSyncExternalStore максимально неудобный и неэффективный. Вместо одного объекта, имплементирующего некий интерфейс, нам нужно передать в этот хук три функции:

function Component() {
  useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
  // ...
}

Как? Как можно было принять такое решение? О чём они думали? Это же абсурд! Я понимаю, что разработчики React'а ненавидят ООП, но не до такой же степени?

Из-за такого API мы вынуждены изменить класс Store. Мы больше не можем переиспользовать объявленные в прототипе методы subscribe и unsubscribe, а должны создавать их для каждого экземпляра отдельно:

class Store implements ExternalStore {

  // новые стрелочные функции для каждого экземпляра 
  // чтобы не потерять this
  subscribe = (subscriber) => {}
  unsubscribe = () => {}
}

Потребление памяти выросло, создание экземпляров — медленнее из-за лишних аллокаций, API — неудобнее. И ради чего?

Возможно, вы впервые слышите об этом хуке, но ведь остальные не лучше; я бы даже сказал — ещё хуже. Вот, например, useState:

function Component() {
  const [state, setState] = useState({ value: 0 });
  const update = () => setState(prev => ({ value: prev.value + 1 }))
  
  return (
    <button onClick={update}>
      {state.value}
    </button>
  )
}

Теперь, каждый раз при нажатии на кнопку будет происходить следующее:

  1. React вызовет функцию Component

  2. Функция Component вызовет useState, что приведет к созданию:
    Двух массивов
    Один создает и возвращает useState, положив в него state и setState, второй создаем мы, чтобы деструктуризацией достать эти значения;
    Одного объекта
    Объект { value: 0 }, который передается параметром в useState, создается при каждом рендере. И хотя он не будет использован, и сборщик мусора его очистит мы все равно потратили время на его создание + какое-то время он будет болтаться в памяти

  3. Создается новая функция update и новая функция, которую мы передает колбэком в setState и дополнительно – еще один объект { value: prev.value + 1 }

Это же безумие! Это издевательство и над сборщиком мусора и над JIT компилятором!

React знает о проблеме — поэтому предлагает костыли типа useCallback или useMemo. Но это не решение, а признание провала.

Я проводил много собеседований и не вспомню случая, когда кандидат на позицию React-разработчика не утверждал бы, что useCallback избавляет от лишних аллокаций. Но ведь это не так. Хук useCallback запоминает аргумент при первом рендере, а все последующие просто создают ненужные объекты типа Function, которые сразу же уничтожаются сборщиком мусора за ненадобностью. При этом документация React'а нагло врёт:

useCallback is a React Hook that lets you cache a function definition between re-renders.

Cache a function definition? Что? Каким это магическим образом вы кешируете объявление функции? Вы переизобрели JavaScript? Это же чушь!

const cache = new Set;

function pseudoUseCallback(calback, deps) {
  cache
    .add(calback)
    .add(deps)
}

for (let i = 0; i < 1000; i++) {
  pseudoUseCallback(() => {}, [])
}

Кажется, что даже джун не будет сомневаться в том, что после выполнения этого кода в кеше окажется 1000 анонимных функций и 1000 пустых массивов. Почему же React пытается нас убедить в обратном, если useCallback точно так же работает [9]?

Я отдельно ненавижу хуки, и у меня нет желания уделять им много внимания. Я лишь хотел показать, что, кроме других проблем, с которыми, я уверен, вы не раз сталкивались, они ещё и жутко неэффективны. Отдельное непонимание у меня вызывают разработчики, утверждающие, что хуки удобны. Серьёзно? Вот это удобно?

  • 🔴 Do not call Hooks inside conditions or loops.

  • 🔴 Do not call Hooks after a conditional return statement.

  • 🔴 Do not call Hooks in event handlers.

  • 🔴 Do not call Hooks in class components.

  • 🔴 Do not call Hooks inside functions passed to useMemo, useReducer, or useEffect.

  • 🔴 Do not call Hooks inside try/catch/finally blocks.

И это удобно?

const [state, setState] = useState({ value: 0 });
const update = () => setState(prev => ({ value: prev.value + 1 }))

Может, и удобно, но React'у, а не разработчику. Кстати, об этом...

Мы в заложниках искусственных ограничений

React уделяет больше внимания не тому, чтобы вам было удобно, а тому, чтобы вам было больно с него слезть. Серьёзно, смотрите сами:

function Component() {
  return (
    <div className="bar">
      <label htmlFor="username">Label</label> 
      <input />
    </div>
  )
}

Опытный React-разработчик даже не заметит подвоха, а неопытный задастся вопросом: это что за атрибуты такие, className и htmlFor? А это, счастливый не-React-разработчик, для того, чтобы тебе было сложнее портировать код на что-то другое!

Звучит как нелепая теория заговора? Да! Но моё объяснение не более нелепое, чем официальное из документации React, например:

Since for is a reserved word in JavaScript, React elements use htmlFor instead.

Правдоподобно, не правда ли? Значит, class тоже зарезервирован? Ну, как вам сказать... Давайте лучше покажу:

class FooElement extends HTMLElement {
  connectedCallback() {
    this.root = this.attachShadow({ mode: "open" });
    this.root.innerHTML = `
      <div>
        For value: ${this.getAttribute("for")}<br/>
        Class value: ${this.getAttribute("class")}
      <div>
    `;
  }
}

window.customElements.define("foo-tag", FooElement);

export default function App() {
  return (
    <foo-tag 
      class="some-class-name" 
      for="some-value"
    >
    </foo-tag>
  );
}

Здесь мы создали кастомный HTML-элемент и отрендерили его React'ом, передав два атрибута — class и for. Убедиться, что всё работает, можно в песочнице [10] или в следующем параграфе документации React [11]'а:

If you use React with Web Components (which is uncommon), use the class attribute instead.

Кстати, о веб-компонентах — их более-менее внятная поддержка в React'е появилась только в 19-й версии [12]. Лет семь сопротивлялись! Всё из-за того, что они реализованы на классах?

Зачем мы тогда пишем tabIndex, className, htmlFor и т.д.? А event.currentTarget?

Нормализация событий – решение выдуманной проблемы

Почему мы вынуждены писать event.currentTarget? Ах да, потому что:

SyntheticEvent… это кросс-браузерная обёртка над нативными событиями.

Но зачем нужна обёртка, если у событий единый стандарт [13]? React застрял в 2010-х, хотя вышел в 2013. Его «нормализация» событий — это попытка решить проблемы IE8 и старых Firefox, которые давно умерли? Нет ни одной причины иметь SyntheticEvent, кроме как сделать код для React'а несовместимым с чем-то другим.

Compiler – апогей абсурда

Да. Compiler прекрасно отражает степень оторванности React'а от реальности. Тут комментировать — только портить. Лучше посмотрите:

const MyOwnObject = {
  get uniqueId() {
    return Math.random()
  }
}


function App() {
  const [count, setCount] = useState<string | number>(0)
  const up = () => setCount(Math.random())

  return (
    <div>  
      <div>Current count {count}</div>      
      <div>Current unique key {MyOwnObject.uniqueId}</div>      
      <button onClick={up}>Update</button>
    </div>
  )
}

Что мы ожидаем увидеть при ре-рендере компонента в качестве uniqueKey? Очевидно, какое-то уникальное число. Но нет! Compiler намертво закеширует значение, полученное при первом обращении к MyOwnObject.uniqueId, и никогда больше к нему не обратится [14]!

У меня просто нет слов, чтобы это комментировать. Страшно представить, сколько вещей может сломать этот компилятор, если он умудрился сломать даже JavaScript!

Представьте, что Date.now [15]() после первого вызова стал бы возвращать одно и то же значение для последующих. Это был бы баг в движке. Это бы сломало интернет. Совсем! А в React — это «фича компилятора». Апогей абсурда! У разработчиков React'а какая-то своя альтернативная реальность?

React больше не про разработчиков. Он про контроль.

Контроль над тем, как вы пишете код, как вы думаете, как вы понимаете JavaScript.

И пока мы молча принимаем его правила — он будет становиться всё более оторванным от реальности.

Поэтому я ненавижу React.

Автор: nihil-pro

Источник [16]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/frontend/434485

Ссылки в тексте:

[1] ModX: https://modx.ru/

[2] jQuery: https://jquery.com/

[3] Fenom: https://github.com/fenom-template/fenom

[4] репозиторий React: https://github.com/facebook/react/tree/main/packages

[5] use-sync-external-store: https://github.com/facebook/react/tree/71b3a03cc936c8eb30a6e6108abf5550f5037f71/packages/use-sync-external-store

[6] ReactHooks.js: https://github.com/facebook/react/blob/71b3a03cc936c8eb30a6e6108abf5550f5037f71/packages/react/src/ReactHooks.js#L188

[7] shared/ReactSharedInternals: https://github.com/facebook/react/blob/main/packages/shared/ReactSharedInternals.js

[8] висят баги от 2014 года: https://github.com/facebook/react/issues?q=is%3Aissue%20state%3Aopen&page=33

[9] точно так же работает: https://codesandbox.io/p/sandbox/yvn2jv

[10] можно в песочнице: https://codesandbox.io/p/sandbox/9ttthh

[11] документации React: https://legacy.reactjs.org/docs/dom-elements.html#classname

[12] 19-й версии: https://github.com/facebook/react/releases/tag/v19.0.0

[13] единый стандарт: https://www.w3.org/TR/uievents/

[14] никогда больше к нему не обратится: https://stackblitz.com/edit/vitejs-vite-px9smmwy?file=src%2FApp.tsx

[15] Date.now: http://Date.now

[16] Источник: https://habr.com/ru/articles/959358/?utm_source=habrahabr&utm_medium=rss&utm_campaign=959358