- PVSM.RU - https://www.pvsm.ru -
Да, я его действительно ненавижу. Мне кажется, что команда 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 даже контрибьютингу всячески препятствует!
Да, 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>
)
}
Теперь, каждый раз при нажатии на кнопку будет происходить следующее:
React вызовет функцию Component
Функция Component вызовет useState, что приведет к созданию:
Двух массивов
Один создает и возвращает useState, положив в него state и setState, второй создаем мы, чтобы деструктуризацией достать эти значения;
Одного объекта
Объект { value: 0 }, который передается параметром в useState, создается при каждом рендере. И хотя он не будет использован, и сборщик мусора его очистит мы все равно потратили время на его создание + какое-то время он будет болтаться в памяти
Создается новая функция 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
foris a reserved word in JavaScript, React elements usehtmlForinstead.
Правдоподобно, не правда ли? Значит, 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 прекрасно отражает степень оторванности 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'а какая-то своя альтернативная реальность?
Контроль над тем, как вы пишете код, как вы думаете, как вы понимаете 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
Нажмите здесь для печати.