Я ненавижу React
- пятница, 24 октября 2025 г. в 00:00:08
Да, я его действительно ненавижу. Мне кажется, что команда React'а презирает разработчиков, и я презираю их в ответ. Все их решения направлены на то, чтобы сделать разработку сложнее, медленнее и непредсказуемее. На сегодняшний день они даже умудрились сломать работу JavaScript. Уму непостижимо, почему им это сходит с рук.
Я знаком с React'ом с первой его версии. Тогда я фрилансил, и моим любимым стеком был ModX с его шаблонами + jQuery. Чуть раньше React'а появился шаблонизатор Fenom, и он был очень крутой. Увидев 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'а — это лабиринт, и для навигации там нужен путеводитель. Смотрите сами.
Идём в репозиторий React'а, в пакет use-sync-external-store и видим это:
import * as React from 'react';
export const useSyncExternalStore = React.useSyncExternalStore;Это всё. Зачем целая директория для этого пакета?
Продолжаем искать. Это хук, значит, искать надо среди хуков, верно? Идём в пакет react, в файл ReactHooks.js и видим это:
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. Давайте посмотрим:
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 года — не потому, что их кто-то другой не может исправить, а потому что это фиктивный 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 точно так же работает?
Я отдельно ненавижу хуки, и у меня нет желания уделять им много внимания. Я лишь хотел показать, что, кроме других проблем, с которыми, я уверен, вы не раз сталкивались, они ещё и жутко неэффективны. Отдельное непонимание у меня вызывают разработчики, утверждающие, что хуки удобны. Серьёзно? Вот это удобно?
🔴 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. Убедиться, что всё работает, можно в песочнице или в следующем параграфе документации React'а:
If you use React with Web Components (which is uncommon), use the class attribute instead.
Кстати, о веб-компонентах — их более-менее внятная поддержка в React'е появилась только в 19-й версии. Лет семь сопротивлялись! Всё из-за того, что они реализованы на классах?
Зачем мы тогда пишем tabIndex, className, htmlFor и т.д.? А event.currentTarget?
Почему мы вынуждены писать event.currentTarget? Ах да, потому что:
SyntheticEvent… это кросс-браузерная обёртка над нативными событиями.
Но зачем нужна обёртка, если у событий единый стандарт? 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, и никогда больше к нему не обратится!
У меня просто нет слов, чтобы это комментировать. Страшно представить, сколько вещей может сломать этот компилятор, если он умудрился сломать даже JavaScript!
Представьте, что Date.now() после первого вызова стал бы возвращать одно и то же значение для последующих. Это был бы баг в движке. Это бы сломало интернет. Совсем! А в React — это «фича компилятора». Апогей абсурда! У разработчиков React'а какая-то своя альтернативная реальность?
Контроль над тем, как вы пишете код, как вы думаете, как вы понимаете JavaScript.
И пока мы молча принимаем его правила — он будет становиться всё более оторванным от реальности.
Поэтому я ненавижу React.