Привет, Хабр! Я уже рассказал, что умею разнообразно писать счётчики.Пришло время сделать шаг вперёд! Сегодня поговорим о том инструменте, который я и миллионы разработчиков используют ежедневно. Речь пойдёт о великом и ужасном ReactJS.
Я пишу на React с 2018 года, делаю это, на мой взгляд, более-менее сносно.Мне нравится тезис о том, что профессионализм заключается не только в умении использоваться достоинства инструмента, но и в умении чётко видеть его недостатки.Поэтому возникла идея сделать что-то типа прожарки React, указав на лично меня раздражающие моменты. С какими-то я смирился, с какими-то нет, что-то научился обходить. Если вас бесит в React что-то, что я не упомянул, не стесняйтесь писать в комментариях, было бы очень интересно сравнить мои ощущения с вашими.
И важное: несмотря на указанные ниже проблемы, я до сих пор считаю React прекрасным и удобным инструментом для создания фронтенда, в частности SPA.Громких слов типа «ReactJS не пригоден для разработки» тут не будет — пригоден, да ещё как! Но… и на Солнце бывают пятна.Приступим.
Твоя библиотека такая толстая…
Во времена Internet Explorer 9-10 библиотека jQuery (помните такую?) считалась очень тяжёлой. Многие рекомендовали стараться избегать её использования, хотя в те дикие времена jQuery была практически незаменима. Размер её бандла составлял страшные на тот момент 72 Кб (минифицированная версия 1.4.2).И разработчики jQuery как могли старались уменьшать его, к примеру, разделяя на несколько бандлов с разным функционалом.
Сейчас на дворе 2025 год и не принято считать байты и даже килобайты. То, что JS-бандл может быть размером с несколько десятков мегабайт, уже мало кого шокирует (хотя я до сих пор могу вздрогнуть, глядя в средства разработчика, но это профдеформация).
В тренде ли React?
Давайте узнаем. Я создал проект на React с помощью Vite и модифицировал конфиг сборки, чтоб выделить React в отдельный чанк:
import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' // https://vite.dev/config/ export default defineConfig({ build: { rollupOptions: { output: { // Добавим выделение библиотеки React в отдельный чанк manualChunks: { react: ['react'], } } } }, plugins: [react()], })
Соберём и получим такой результат:
vite v6.2.1 building for production... ✓ 30 modules transformed. dist/index.html 0.54 kB │ gzip: 0.33 kB dist/assets/react-CHdo91hT.svg 4.13 kB │ gzip: 2.05 kB dist/assets/index-n_ryQ3BS.css 1.39 kB │ gzip: 0.71 kB dist/assets/react-yBjzXJbu.js 6.79 kB │ gzip: 2.72 kB dist/assets/index-CkN_Kf_f.js 137.38 kB │ gzip: 44.22 kB ✓ built in 380ms
Сам по себе React занимает буквально меньше 7 Кб! Это даже меньше preact (~ 11 Кб), который заявляет себя легкой альтернативой. Так что получается, врут о том, что React большой и прожорливый? Но что за оставшиеся 137 Кб…
Очевидно, что-то тут не так. Поглядим на наш список зависимостей и выделим ещё один чанк:
reactDOM: ['react-dom/client']
И результат:
✓ 30 modules transformed. dist/index.html 0.62 kB │ gzip: 0.35 kB dist/assets/react-CHdo91hT.svg 4.13 kB │ gzip: 2.05 kB dist/assets/index-n_ryQ3BS.css 1.39 kB │ gzip: 0.71 kB dist/assets/index-Xo5bg2S4.js 2.59 kB │ gzip: 1.33 kB dist/assets/react-yBjzXJbu.js 6.79 kB │ gzip: 2.72 kB dist/assets/reactDOM-CKhEkZwU.js 134.78 kB │ gzip: 43.26 kB
Почти 135 Кб это React-DOM! И беда в том, что без него сам по себе React не заработает — просто не сможет отрендерить DOM-дерево. Что скрывается в без малого 135 килобайтах минифицированного кода — я представляю очень примерно и, честно сказать, страшно вникать в это подробно. Факт в том, что вам недостаточно установить пакет react, чтоб пользоваться React. Вам нужно добавить на страницу 135 Кб кода, чтоб хоть что-то отобразить!

Изначально, кстати, разделения библиотеки не было и React поставлялся одним файлом. React-DOM был выделен в версии 0.14 с целью сделать архитектуру React более модульной. Это (по задумке) позволяет грамотно использовать его не только на клиенте, но и на сервере или в мобильной разработке. Насколько сильно было нужно разделение одного бандла в 140 Кб на два в 7 и 135 Кб — не мне судить, возможно, оно помогло в создании того же React-Native. Только вот… React для своего монструозного размера умеет удивительно мало. У него нет роутера, нет средств работы с http-запросами (в 19 вроде появилось), нет встроенной библиотеки компонентов или хотя бы набора стилей! Упрощённо, внутри React только очень прокачанный шаблонизатор, средство работы с DOM (надо признать, мощнейшее) и некое подобие стейт-менеджмента в лице хуков.
И всё это в preact (с рендером в DOM) спокойно вмещается в 11 Кб, что в 12 раз меньше прародителя. Бандл Vue с примерно теми же возможностями (и многими приятными фичами типа изоляции стилей из коробки или слотов) занимает 60 КБ. Angular, тоже монструозный, сравним с react-dom — бандл занимает ~ 160 Кб, но примерно 60 из них это встроенный роутер, которого в стандартной поставке React нет, да и по возможностям Angular гораздо богаче. Вот и получается, что из всех популярных фронтенд-фреймворков (да знаю я, что React считается библиотекой, знаю) React оказался самым тяжёлым. Причём, все эти килобайты не просто загружаются, а активно используются. Неужели механизмы VirtualDOM и Reconciliation настолько огромны? И если да, то остаётся ли какой-то выигрыш в производительности с таким большим оверхэдом? Судя по бенчмаркам — всё очень плохо и React стабильно медленнее большинства аналогов без VirtualDOM (особенно впечатляет в этом плане Solid, который очень похож на React снаружи, но кардинально другой внутри). Почему — тема для отдельной статьи.
JSX это почти HTML. Но не HTML.

Понятно, что JSX = JS + XML, я это знаю. Но в большинстве задач JSX используется именно как заменитель HTML-разметки и по факту куда ближе к HTML, чем к XML. Из этого и будем исходить.
Очевидно, что язык программирования JavaScript и язык разметки HTML — очень разные сущности. Поэтому соединение одного с другим, разумеется, будет компромиссом. JSX как раз прекрасная иллюстрация такого компромисса.
Например, вы не должны использовать атрибут class — вместо него нужно писать className, ведь class — это ключевое слово JS (причём, оно было таковым ещё до появления классов в языке) . Аналогично с for-htmlFor — окей, тут тоже понятно. Запомнить 2 отличия, не проблема.
Тут есть 2 забавных момента. Первый: вы можете писать class, и это будет работать, но React поругается в стиле «не надо так». Второй:необходимости в таком поведении не было. В Preact, который тоже использует JSX, например, вполне использовались традиционные HTML-ные class и for (тут смешно, что ради совместимости с React разработчикам Preact пришлось перейти на htmlFor).
Или ещё момент — обязательное требование к единственности корневого элемента.React при всей его свободе местами принципиален. Он не прожуёт наличие более одного корневого элемента и потребует обернуть всю конструкцию в хотя бы React.Fragment (сейчас, к счастью, сокращающийся до <></>).Я примерно понимаю, почему неявно добавлять этот самый <></> было бы (наверное) не очень хорошей идеей, но меньше раздражения необходимость делать это не вызывает.
Но стоило нам смириться с этими различиями, на сцену выходят ещё и ещё. HTML case-insensitive язык — что тэги, что атрибуты можно писать в каком угодно регистре.Такой код в HTML будет вполне корректен и будет работать ровно так, как должен (что, разумеется, совсем не повод так писать):
<!DOCTYPE html> <html lang='en'> <head> <title>CaSe INsensITIVe</title> </head> <body> <DIV> <sPaN>Hello</SpAn> <button ONCLICK='alert("hello")'>HELLO</button> </DIV> </body>
Если не верите — проверьте на валидаторе W3C. Код абсолютно валиден, скриншот валидатора ниже

А вот React+JSX такое уже не проглотят. В принципе, это не беда, ведь кому в здравом уме придёт в голову играться с регистром в HTML-тэгах. Но! Вы должны чётко помнить, что вместо onclick надо писать onClick (и никак иначе), с другими эвентами аналогично, атрибут readonly пишется как readOnly (и никак иначе) и т п. Все эти различия можно запомнить, и вы их рано или поздно запомните, но… Зачем же тогда JSX так похож на HTML? То, что мы не можем просто взять и вставить готовую вёрстку в компонент, очень бесит и вынуждает использовать конвертеры HTML<->JSX.
А ещё имя компонента обязательно должно начинаться с большой буквы.
const comp = () => { return <div>comp</div> } const Comp = comp; const App = () => { return (<> <Comp /> {/* Ok */} <comp /> {/* Ошибка */} </>); }
Впрочем, такое поведение уже настолько привычно, что проникло вообще везде. Просто запомните и живите с этим.
Различия JSX и HTML не фатальные, но их много и они раздражают. И то, что со временем к этим мелким пакостям привыкаешь, совершенно не повод не побухтеть по этому поводу.
Радикальное решение вопроса ошибок
На мой взгляд — самая жуткая недоработка нашей любимой библиотеки.
Знакомая любому опытному разработчику React картина — изменяешь код какого-то небольшого компонента и… получаешь страницу с белым экраном.Это может быть реальная ошибка, опечатка или просто некорректный компонент — плевать, приложение по умолчанию падает полностью.
Вот простой пример:
import { useEffect, useState } from 'react'; export const ErrorItem = () => { let [value, setValue] = useState('hello'); useEffect(() => { setTimeout(() => { setValue({message: 'hello'}) }, 2000) }, []); return <div>{value}</div> }
Если вы добавите компонент ErrorItem, спустя пару секунд обнаружите, что всё ваше приложение упало. Не конкретный компонент, а полностью всё, до пустого экрана.React при ошибке в компоненте просто отмонтирует вообще всё DOM-дерево, где он расположен. Причём даже причина ошибки не столь очевидна — придётся просто запомнить, что в JSX-разметку нельзя передавать объект, это вызовет ошибку рендера (возможность просто вывести этот объект как JSON опять же не влезла в 135 Кб React-DOM, видимо).
Ладно, наверняка у такого ограничения есть причины.Но разрабатывая на Vue, я просто кайфовал с возможности при отладке тупо вывести объект на экран. React так не умеет, приходится оборачивать в JSON.stringify.
Для сравнения, аналогичный код на Vue
<script setup> import { ref } from 'vue' let message = ref({hello: {value: 'hello'}}) setTimeout(() => { message.value = {hello: null} }, 2000) </script> <template> <div> <span>Hello</span> <div>{{message.hello.value}}</div> </div> </template>
Если вы подключите компонент из этого файла, то спустя 2 секунды вы получите пустоту только на месте, в котором должен был быть компонент. Остальное приложение продолжить корректно работать. ТАК И ДОЛЖНО БЫТЬ! ПО УМОЛЧАНИЮ! Простите за капс, но то, что в React это не так, пожалуй, самое грустное, с чем приходится сталкиваться при разработке.
На самом деле, формально есть решение данной проблемы — ErrorBoundary, описание в документации React. Но это же издевательство! Во-первых, готового компонента ErrorBoundary в поставке React нет — его придётся писать самому либо искать библиотеку. Во-вторых, ErrorBoundary можно создать только с помощью классовых компонентов, потому что у функциональных просто нет необходимых возможностей.А в-третьих — оборачивать придётся вообще каждый компонент, иначе рухнет всё внутри.Просто рухнет несколько более красиво.
И весь ужас в том, что это приобретённый недостаток.Такое поведение реализовано в React 16 и было цинично названо Better error handling.В принципе, до этого обработка ошибок в React тоже была не фонтан, но это улучшение оказалось сродни отрубанию головы при головной боли.Хорошо, хоть сами разработчики React признают, что «it’s probably not the ideal user experience».
React отдельно, типы отдельно
Интересный факт — React чуть ли не единственная популярная JavaScript-библиотека для построения интерфейса, написанная не на TypeScript. Vue, Angular, SolidJS и большинство игроков поменьше были либо переписаны на TypeScript, либо написаны на нём изначально.
Я не упомянул топ-4, Svelte. Его тоже с некоторой оговоркой можно отнести к написанным на TypeScript. Несмотря на то, что в кодовой базе сплошь JS файлы, внутри них вполне используются типы TypeScript посредством JsDoc. Наверное, разработчику Svelte такой подход кажется удобным.
А что же React? Если вы зайдёте на страницу git-репозитория ReactJS, то увидите такую картину:

26.5% TypeScript не должны вводить вас в заблуждение — это, в основном, React Compiler. Сам React написан с помощью Flow — менее популярной реализации идеи «JavaScript с типами» от Facebook (что логично, ибо корни React там же, в глубинах запрещённой в РФ экстремистской организации Meta).
Казалось бы, ну и что с того? Какая разница, на чём написано, в желудке всё перемешается в итоге всё транспилируется в JavaScript. Однако проблема в том, что Flow хоть и местами очень похож на TypeScript (как человек больше года поддерживавший проект на Flow, говорю — различия почти всегда косметические), в общем случае с ним не совместим. Тайпинги для TypeScript в React отсутствуют — нет TypeScript, нет и тайпингов. Для React их нужно ставить отдельно пакетом @types/react. И если у вас не возникали проблемы при обновлении React, связанные с некорректными типами — я вас поздравляю, вы, видимо, более везучи и квалифицированы, чем я. Я сталкивался с неверно прилетающей версией @types/react очень часто,и всегда это было больно. Впрочем, сейчас по ощущениям проблема стала несколько менее актуальной — то ли я привык, то ли разработка react и @types/react окончательно синхронизировалась.
Хуки — и так сойдёт!
Тот самый случай, когда идея крутая, а реализация… Ну, не такая крутая, скажем так. За годы, прошедшие с появления хуков, мы к ним уже привыкли, но если вдуматься, граблей в их реализации накидано немало.
Начнём с базы: правила хуков. Их можно использовать:
-
только в функциональных компонентах и других хуках
-
только на верхнем уровне приложения — нельзя использовать хуки в условиях, циклах и любых других местах, где хук может быть не вызван при каком-то условии
К первому правилу в целом вопросов нет, это вполне логично. Но откуда взялось второе?
Разгадка проста: React полагается на порядок вызова хуков! Иными словами, состояние хуков внутри хранится в строго определённом порядке. Если порядок изменится, то состояние (скорее всего) станет невалидным. Поэтому хук вызывается всегда, нужен ли он или нет.
Не кажется ли вам это костылём? Лично мне кажется. Но костыль это всем известный и привычный, с ним уже все смирились. Смиримся и мы.
Или необходимость вручную указывать зависимости хуков в массиве? Нет, в этом есть какой-то шарм, но… Это выглядит как ручная правка убогости самого механизма встроенных хуков! Меня, например, очень веселит тот факт, что eslint при помощи определённых правил может определить нехватку каких-то зависимостей у хука, а сам React нет. Хотя тут я немного лукавлю — уже упомянутый React Compiler вроде бы научился где надо подставлять useCallback, useMemo и memo. Но пока, судя по всему, React Compiler ещё не готов до конца, поэтому раскидывать зависимости, useCallback и прочая прочая прочая придётся руками.
Но у меня есть и свой «любимчик». useEffect. При внешней простоте его поведение настолько неочевидно, что требует подробного разбора от самого Дэна Абрамова. Возможно, ваш опыт с useEffect удачный, конечно, но моим главным правилом для этого хука стало «если можешь его не использовать — не используй».
Понятно, что мы привыкли к магии: что при передаче пустого массива зависимостей useEffect вызовется лишь раз, что если не передать зависимости вообще, он будет вызываться каждый рендер. И моё любимое — очистка используемых ресурсов как… вызов функции, которую возвращает колбэк useEffect. Если у первых двух кейсов даже есть какая-то логика, последнее максимально не интуитивно и, например, лишает нас возможности передать в useEffect асинхронную функцию. Нет, мы к этому уже привыкли и относимся как к должному, но если вдуматься — а почему это сделано именно так неочевидно, а не, скажем, опциональным параметром? Я так и не понял. Напишите, если поняли вы.
Но всё же — хуки это вполне неплохо. Да, к …кхм… особенностям встроенных хуков придётся привыкнуть, но возможность выделить логику в отдельный хук и переиспользовать его определённо того стоит. Хотя, разумеется, с этим вполне справляется и хороший стейт-менеджер.
Заключение
Изначально я думал, что недостатки React я буду подбирать с трудом и мне придётся просить помощи у коллег, чтоб статья не была уж слишком маленькой. В итоге, пришлось волевым решением себя останавливать и значительно сокращать статью. Означает ли это, что React настолько плох? Мне трудно ответить на этот вопрос. Несмотря на перечисленные недостатки, свою работу React делает и делает её хорошо — тысячи прекрасно работающих приложений и огромная экосистема тому подтверждение. К неприятным мелочам (и не мелочам) со временем привыкаешь, интуитивно учишься их обходить и познаёшь некий дзен React — что бы это ни значило.
Если у вас есть свои болевые точки в React или других инструментах, если вы не согласны с высказанным в статье или просто хотите предложить переписать всё на $mol — буду рад видеть вас в комментариях.Спасибо за внимание!
ссылка на оригинал статьи https://habr.com/ru/articles/892406/
Добавить комментарий