javascript

От React всё также веет безумием, но все об этом молчат

  • понедельник, 14 июля 2025 г. в 00:00:04
https://habr.com/ru/companies/ruvds/articles/926286/

Недавно работал над хобби-проектом, который описал в другой своей статье. В процессе его реализации у меня возникло желание чиркануть пару абзацев о том, почему React — отстой, но в итоге я не смог удержаться и решил высказаться по полной…

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

Древний Angular

Когда я был ещё джуном и только осваивал профессию, мне довелось работать с Angular, популярным «коммерческим» фреймворком JS. В те времена это была достаточно хорошая технология. Явно крупнейший фреймворк JavaScript на тот момент. Плюс в копилку его заслуг можно положить тот факт, что это, пожалуй, был первый фреймворк для разработки в веб-среде. До этого были только «библиотеки», так что Angular не только первым предоставил нам набор функций, но и стал реальным фреймворком, на котором можно было создать веб-приложение.

Но всё познаётся в сравнении, и Angular был хорош только потому, что значительно обходил прежние решения. В то время были и другие фреймворки для разработки одностраничников вроде Backbone и Knockout, но их след в истории оказался не столь значительным. Реальным же соперником, которого победил Angular, был jQuery.

Несмотря на то, что jQuery был лишь обёрткой для (тогда довольно паршивых) HTML DOM API, он всё равно стал эталонным решением для создания комплексных веб-приложений. Принцип его работы был довольно прост — вручную и императивно создаёте HTML-элементы на JS, затем изменяете их, перемещаете куда надо и делаете всё необходимое, чтобы сайт работал интерактивно так, будто это приложение.

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

Затем появился Angular и всё уладил. Теперь вы могли направить свои силы на написание UI и логики приложения вместо того, чтобы вручную собирать отдельные кусочки HTML. Эта библиотека фреймворк поистине стал революционным, так как у нас, наконец, появился инструмент для создания реально БОЛЬШИХ интерактивных приложений. Вот лишь часть из его магических свойств:

A) Компоненты. Если быть точнее, то назывались они «директивами», так как в нём использовалась странная система именования. Но как бы то ни было, вы могли написать с помощью HTML и JS простой файл, представляющий элемент UI, и использовать его в разных местах приложения.

Б) Двухсторонняя привязка (two-way binding). Суть в том, что после определения переменной впоследствии при её изменении обновлялись все связанные области в UI. Позднее люди начали жаловаться на такой всенаправленный поток данных, считая его неудачным. Тогда возникла тенденция к использованию односторонних привязок (сверху вниз), что с технической стороны звучит как более удачное решение, но на практике только всё усложнило и привело к дискуссии, которая кончилась тем, что сегодня мы используем Redux. Так что, спасибо.

На своей первой работе я как раз наблюдал эти тренды воочию, когда занимался переписыванием одного такого неуклюжего приложения jQuery на Angular. И сам этот процесс, и конечный результат меня порадовали.

Но были и неприятные моменты. Например, мне не понравилось переписывать те же самые экраны на Angular 2 через несколько лет, и я рад, что успел уйти из той компании до того, как они могли заставить меня переписывать всё это в третий раз — уже на React.

Знакомимся с React

Позднее у меня появилась возможность освоить React и даже использовать этот инструмент профессионально в паре проектов.

Я всё ещё помню, как свежо он выглядел на первый взгляд. Тогда React ярко контрастировал с актуальным на тот момент фреймворком Angular 2, который требовал полностью переписывать код своего предшественника, но теперь с удвоенным объёмом бойлерплейта, односторонней привязкой, использованием TypeScript и паттернов reactive/observable. Сами по себе все эти возможности прекрасны, но чёрт возьми, они так всё усложняли, замедляли работу, сборку и выполнение кода.

React же качнул маятник обратно в сторону простоты, и люди это оценили. Какое-то время простота действительно сохранялась. React набрал популярность и стал библиотекой #1 для создания одностраничников.

Да, теперь мы снова использовали термин «библиотека», демонстрирующий простоту этого инструмента. Но нельзя адекватно создать комплексное приложение с помощью одной библиотеки — для обработки всех его задач потребуется несколько, а также полноценная структура кода. Подход React в стиле «напитки приноси с собой» подразумевал, что вы создаёте фреймворк сами со всеми вытекающими недостатками.

В конечном итоге все приложения на React получались индивидуальными. В каждом из них создавался «фреймворк», состоящий из собранных по сети рандомных библиотек.

Все приложения, с которыми мне не повезло работать в то время, подводили меня к одному выводу — для этого бы лучше подошёл даже Angular 2. «Ядро» JSX всегда было твёрдым, но всё сопутствующее представляло полную неразбериху.

В итоге я бросил это гиблое дело и занялся написанием бэкенда на Java. Думаю, мой выбор говорит сам за себя.

Только я решил, что покончил с этим...

Говорят, что научиться — не значит понять. Очевидно, я так до конца и не понял, поэтому недавно снова окунулся в дебри React.

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

Архитектура, компоненты, состояние

Начнём с архитектуры, которую тебя вынуждает использовать React. Как я уже говорил, React — это просто библиотека, поэтому она ни к чему тебя не обязывает. Но всё же неявные ограничения, связанные с JSX, раскрывают некоторые паттерны. Давным-давно мы говорили об MVC (Model-View-Controller), MVVM (Model-View-ViewModel), MVP (Model-View-Presenter) — все из которых представляют лишь вариации на тему. И какой же из них подразумевает React? А никакой. Думаю, в нём заложена новейшая парадигма, которую можно назвать буквально «архитектура на основе компонентов».

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

Но где-то в процессе своего развития эта библиотека начала мудрить. Для простой «библиотеки UI» в ней явно образовалось многовато сложной терминологии. А для библиотеки, никак не связанной с «функциональным программированием», в React присутствует уж очень много названий из этой сферы.

Начнём с состояния. Если вы создаёте нисходящее дерево компонентов, то наверняка решите и состояние тоже передавать сверху вниз. Но на практике в свете присутствия огромного числа мелких компонентов всё это создаёт путаницу, так как приходится тратить кучу времени и кода только на связывание элементов данных, направляя их туда, куда нужно.

Эта проблема была решена путём «подгрузки» (англ. sideloading) состояния в компоненты с помощью хуков. Не слышал, чтобы кто-то на это решение жаловался, но я вас умоляю, вы что, серьёзно? Вы говорите, что любой компонент может использовать любой элемент состояния приложения? Хуже того, любой компонент может вызывать изменение состояния, способное повлиять на состояние любого другого компонента.

Как это решение вообще прошло код-ревью? Вы, по сути, используете глобальную переменную, только при более изощрённых правилах изменения состояния. И это даже не правила, а просто условная церемония, так как ничто не мешает изменять состояние из любой части программы. Люди реально думают, если назвать что-то заумным именем типа «редьюсер», то это внезапно станет Good Architecture™?

Но если подходы с нисходящим построением и «подгрузкой» являются неудачными, то что станет решением? Честно, даже не знаю. Мне на ум приходит одна мысль: «Если мы не можем решить это изящно, то, может, вся «архитектура на основе компонентов» была ошибкой, и нам не следовало нарекать её образцом «удачного дизайна», прекращая поиск более грамотных решений. Быть может, на сей раз нам действительно нужен другой фреймворк JS, пробующий более удачные подходы.

Хуки React

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

Даже не буду углубляться в то, что люди называют компоненты «чистыми функциями», но в итоге засовывают в них крохотные чёрные ящики с состоянием. А с учётом их составной природы, это больше подобно наслоению крохотных чёрных ящиков.

Я же хочу заострить внимание конкретно на useEffect. Реализуемый с его помощью «побочный эффект» прост и понятен. Вы изменяете состояние, после чего требуется совершить некое внешнее действие, например, отправить результаты в API. Это разделение между «важной составляющей приложения» и «побочными эффектами» имеет смысл — в теории. Но сможете ли вы также чисто разделить их на практике?

Больше всего в useEffect меня раздражает то, что этот хук используется для задачи «выполнить что-то после монтирования компонента». Я понимаю, что после переезда React с классов на хуки это была ближайшая альтернатива componentDidMount, но вот скажите мне — разве это не огромный хак?

Вы используете хук, реализующий «побочные эффекты», для инициализации компонента? Хорошо, если вам нужно сделать из хука вызов, то я согласен, это будет побочный эффект. Но ведь вызов API… он устанавливает и состояние тоже. В итоге абсолютно безобидный хук для создания побочных эффектов по факту управляет состоянием компонента. Почему никто не указывает на это безумие?

Более того, если бы вы хотели установить зависимость от этого состояния и делать что-то после него, то вы бы…определили ещё один useEffect, зависящий от результата выполнения первого.

Screenshot showing hard-to-parse useEffect chain
Screenshot showing hard-to-parse useEffect chain

Этот код я взял из рабочего приложения в компании, которая недавно была куплена за несколько десятков миллионов долларов США. Я его чуть подредактировал, заменив реальные сущности на упрощённые House и Cat. Но вы просто взгляните и попытайтесь проследить, в какой последовательности этот код выполняется. Когда будете готовы, загляните под спойлер ниже, чтобы увидеть правильный ответ.

Ответ

И что мы здесь имеем? Серия изменений состояния, которая в противном случае была бы простым императивным кодом, теперь… разбросана по двум асинхронным функциям, и единственной подсказкой о порядке выполнения является «массив зависимостей» в нижней части каждой. В итоге читать этот код по факту надо снизу вверх.

Я помню, как промисы JS со своими then называли неповоротливыми, да и раньше у нас уже была проблема с «адом обратных вызовов» — но ничто не сравнится с этим.

Думаю, эти сложности можно устранить двумя путями: а) переместив всё это в отдельный файл, что лишь скроет проблему; или б) возможно, с помощью Redux или чего-то аналогичного, но здесь у меня недостаточно опыта для каких-то конкретных предложений.

«Паттерны»

Всё это воедино выглядит ужасно и никак не пахнет той простотой, которую разработчики React обещали в примере «Hello world». Но и здесь ещё не всё. Намедни я прочёл статью под названием «The Most Common React Design Patterns». Не знаю, чего я ожидал, но в итоге был шокирован сложностью описанных там паттернов и тем, сколько умственных усилий необходимо, просто чтобы разобраться в происходящем. Причём всё это лишь для того, чтобы отрисовать элементы на экране.

И самое странное в том, что автор статьи даже этого не признаёт. Вся эта сложность воспринимается им как должное. Похоже, люди действительно безропотно создают свои UI таким образом.

А ведь некоторым и этого мало, они идут ещё дальше и пишут «CSS вместе с JS», и даже получают за это деньги. Я согласен, JSX сразу продемонстрировала, что «разделение задач» не означает «разделение файлов», и что вполне нормально писать HTML- и JS-код в одном файле. Но засовывать туда ещё и CSS с использованием строгой типизации? Разве не перебор?

Почему?

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

Я вновь углубляюсь в чертоги памяти и вспоминаю свою первую работу и коллегу, который упомянул проект переезда на jQuery. Невероятно опытный бэкенд-инженер, архитектор, да и просто уважаемый парень в сфере ПО.

Мне же больше запомнились не его технические решения, а огромное количество критики, которую он выражал в отношении всего, что мы делали во фронтенде. Когда он видел какое-то решение на Angular, то звучал примерно такой комментарий: «Чем вы тут вообще занимаетесь? Неужели нельзя сделать всё как-то проще?»

И дело было не в нас — наша команда имела неплохой опыт в разработке. Просто в то время глазами бэкенд-инженера вся система Angular выглядела совершенно неадекватной.

Сегодня мне примерно столько же лет, сколько было тому парню тогда, и я пишу статью о том, что Angular React — отстой. Думаю, некоторые вещи неизбежны.

Но взглянем с более общего ракурса и попробуем понять, почему так случилось.

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

Но здесь я поспорю, так как на деле подобный ход накладывает достаточно хлопот. Просто мы привыкли к модели использования одностраничников по умолчанию и забыли о более простых альтернативах. Запустить простецкую страничку с отрисовкой на сервере будет куда легче, чем даже подумать о React. В этом случае нет издержек на коммуникацию с API, фронтенд получается лёгким, код UI можно строго типизировать (если бэкенд тоже строго типизирован), можно делать рефакторинг по всему стеку, повысится скорость загрузки и удобство кэширования, так как некоторые компоненты будут статичны и одинаковыми для всех пользователей, то есть их можно будет загружать один раз. И это лишь часть списка.

Хотя тогда вы уже не сможете так же гибко реализовывать сложную интерактивную логику по требованию продакт-менеджера. Но и это не совсем так. Я уверен, что вы сможете довольно далеко продвинуться путём «постепенного расширения» JS-кода, пока управление состоянием не станет достаточно сложным, чтобы оправдать подключение к процессу React.

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

И мой ответ на вопрос «Почему?», как ни удивительно, отводит праведный гнев от React, защищая не только эту библиотеку, но также Angular, jQuery и всех их предшественников. Думаю, плохой код объясняется тем, что создание интерактивного UI, где любой компонент может обновлять любой другой компонент, просто является одним из сложнейших аспектов в разработке ПО.

Ради сравнения подумайте о любой другой системе, которую используете повседневно. У вашего кухонного крана есть два источника ввода — холодная и горячая вода, объединяемых в одном выводе. У кухонного миксера или строительной дрели может иметься пара кнопок, и что бы вы с ними ни делали, это будет влиять на действия вращающегося узла. У газовой плиты может быть до пяти регуляторов и такое же число выходов, что уже начинает немного пугать.

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

Так что насчёт всего моего наезда на React… По сути, это вовсе не вина библиотеки, как и не вина Angular или jQuery. Какую бы технологию вы ни выбрали, она неизбежно скрючится под гнётом невыносимой сложности реактивных UI.

Как?

Как это исправить? Боюсь, для решения этой проблемы мне не хватит знаний и опыта, но я могу высказать пару идей. Если мы усвоим эту ментальную модель «входов/выходов» на веб-странице как основу, то можно будет начать работать над сокращением числа этих входов и выходов.

В отношении входов я, например, предложу: «Сократите количество кнопок», что может не всегда оказаться осуществимым. Но здесь связь очевидна — чем меньше у вас функциональных возможностей, тем легче управлять кодом.

И вроде это достаточно понятно, чтобы лишний раз не напоминать — не так ли? Знают ли продакт-менеджеры, что добавление трёх кнопок вместо двух повышает количество багов на 5% и на 30% усложняет последующее проектирование и реализацию работы страницы? Эти значения никто даже не измеряет, но я считаю, что они вполне могут оказаться верны.

Почему, если я скажу вам, что нужно добавить в бэкенд Redis, вы возразите в духе: «Нет, нужно сдерживать техническую сложность», а если продакт-менеджер попросит добавить в приложение глобальный фильтр, который можно применить отовсюду и ко всему, вы просто опустите голову и напишите такого кодомонстра, от которого ещё с десяток лет придётся избавляться.

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

А вот со стороны выходов ситуация несколько иная. Я пишу всё это и понимаю, что создание страницы с отрисовкой на сервере, по сути, сведёт эту страницу к одному выводу. С чем бы вы ни взаимодействовали, это будет приводить к повторной отрисовке всей страницы. То есть, как бы иронично это ни звучало, но удаление из замеса всего функционального кода React фактически сделает отрисовываемую сервером страницу чистой функцией состояния. Если вы можете позволить себе такую роскошь, то отсутствие состояния фронтенда обеспечит значительный выигрыш в простоте.

Если же вам в таком отрисовываемом на сервере «приложении» вдруг потребуется скриптовая логика, разумно будет добавить её только в самых важных местах. И чем меньше таких мест, тем лучше.

Сперва я подумал, что удачным названием для такой модели будет «островки интерактивности». Потом погуглил, и оказалось, что такое понятие уже есть. Тем не менее в этой статье всё равно упоминаются Preact, SSR, файлы манифеста, поэтому я не уверен, что мы говорим об одном и том же. Люди склонны всё излишне усложнять.

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

Итак, мой непроверенный подход к получению чистого и обслуживаемого кода во фронтенде звучит как «Отрисовывай всё на сервере и подключай React только там, где это реально необходимо».

Хуже точно не станет.