⚛ Реакт – хлам, и я вам это докажу!
Современные фронтенд-фреймворки обещают вам быструю разработку, простую интеграцию и избавление от всех возможных проблем. На самом деле обычно вы получаете совсем другое.

Статья публикуется в переводе, автор оригинального текста Джейсон Найт.
На клиентской стороне фреймворки разрушают юзабельность и доступность, так как многие важные вещи (вроде корзины покупок) просто не могут работать без JavaScript и не имеют никакой адекватной "изящной деградации". Хуже того, они скрывают реальные взаимодействия с DOM и добавляют вашим приложениям ненужную сложность.
Конечно, некоторых из этих проблем можно избежать – смотри Gatsby – но это только еще больше запутывает то, что без фреймворков можно сделать проще, чище и понятнее.
Фронтенд-фреймворки в лучшем случае вас дезинформируют, а в худшем – нагло лгут!
Ложь
Во что вы верите?
Прямое взаимодействие с "живым" DOM медленно!
Ха! Нет никакого очевидного преимущества их утомительного "виртуального DOM" перед прямым изменением обычного DOM. Это даже медленнее, потому что требуется проанализировать изменения, прежде чем все равно внести их в живой документ. Просто возьми и измени!
Не храните данные в DOM, это небезопасно!
100% ложь! Вы в любом случае собираетесь поместить их туда, и не имеет значения реально или "виртуально". Это не влияет не только на скорость, но и на безопасность.
DOM слишком сложен для нормальных людей
Серьезно? Вы сравниваете простое дерево объектов с мешаниной кода, свойственной всем фронтенд-фреймворкам? Эти странные утверждения о том, что ванильный код "сложный и непонятный" происходят из какого-то иррационального страха разработчиков перед объектами.
Вам говорят – "ты слишком тупой для всего этого" – и вы верите.
Эти и многие другие утверждения фронтенд-фреймворков в конечном счете сводятся к одному и тому же. Вам предлагают писать больше кода более сложным способом и говорят, что это "проще" и "лучше" чем ванильные эквиваленты. Да кому нужны эти ваши HTML, CSS, JavaScript?
Докажи!
Легко! Возьмем два самых "мясистых" примера из ранних туториалов React: крестики-нолики и калькулятор температуры. В них достаточно логики, и при этом они не являются критичными компонентами как форма контактов или корзина, а значит могут на 100% полагаться на JS без фоллбэков.
Make и другие библиотечные функции
Фреймворки обычно ценят за то, что можно сделать самостоятельно с нуля за пару минут, что мы и сделаем.
Первая функция – make
. Она похожа на знакомый вам create
из React, но может принимать JSON, чтобы создавать за один раз большие DOM-деревья. Это примерно эквивалентно тому, во что компилируется JSX.
function make(tagName, data) { var e = document.createElement(tagName); if (data) { if ( data instanceof Array || data instanceof Node || ("object" !== typeof data) ) return makeAppend(e, data), e; if (data.append) makeAppend(e, data.append); if (data.attr) for ( var [name, value] of Object.entries(data.attr) ) setAttribute(e, name, value); if (data.style) Object.assign(e.style, data.style); if (data.repeat) while (data.repeat[0]--) e.append( make(data.repeat[1], data.repeat[2]) ); if (data.parent) data.parent.append(e); } return e; }
Первым параметром передается тег, а вторым либо массив дополнительных инструкций для создания дочерних элементов, либо объект с рядом опциональных свойств:
append
с той же логикой для добавления потомков рекурсивно,attr
с атрибутами,style
для установки стилей,repeat
для создания группы элементов,parent
для указания родительского элемента.
Для большей надежности можно добавить еще поле placement
, чтобы указать конкретное место размещения внутри родителя.
Пример использования
Добавим элемент thead
внутрь таблицы table#test
, а него tr
с несколькими ячейками th
:
make("thead", { append : [ [ "tr", [ [ "th", { scope : "col", append : "Item" } ], [ "th", { scope : "col", append : "Quntity" } ], [ "th", { scope : "col", append : "Unit Price" } ], [ "th", { scope : "col", append : "Total" } ] ] ] ], parent : document.getElementById("test") ] );
Выглядит довольно просто, а главное наглядно. Эта функция использует еще два маленьких вспомогательных метода:
function makeAppend(e, data) { if (data instanceof Array) { for (var row of data) { e.append(row instanceof Array ? make(...row) : row); } } else e.append(data); } function setAttribute(e, name, value) { if ( value instanceof Array || ("object" == typeof value) || ("function" == typeof value) ) e[name] = value; else e.setAttribute(name === "className" ? "class" : name, value); }
makeAppend
похожа на Element.append
, но принимает массив того, что нужно добавить. Если в потоке данных она встречает массив, то передает его обработку функции make
.
setAttribute
– это прокачанный Element.setAttribute
, способный принимать не только строки. Если он получает массив, объект или функцию, то назначает их напрямую как свойства элемента. Также мы заменяем атрибут className
на class
, как и оригинальный React.
Помимо этого нам понадобится еще одна функция для очистки:
function purge(e, amt) { var dir = amt < 0 ? "firstChild" : "lastChild"; amt = Math.abs(amt); while (amt--) e.removeChild(e[dir]); }
Она удаляет amt
потомков с конца родительского элемента e
. Если передать отрицательное число, то потомки будут удаляться с начала.
Эти 4 маленьких функции покрывают 80% всех задач при работе с DOM.
А теперь начнем!
Tic Tac Toe
Вариант React:
https://codepen.io/gaearon/pen/gWWZgR?editors=0010
Вариант Vanilla:
https://codepen.io/jason-knight/pen/qBqwrwo
Например, вместо бессмысленного div.board-row
в их сгенерированной разметке куда правильнее было бы использовать группу полей fieldset
. А этот state
отслеживает массу ненужных дополнительных данных, в то время как достаточно записывать только ходы – это будет быстрее и чище.
Хуже всего то, что вы просто не видите реальные записи в DOM и должны на 100% полагаться на их код и доверять ему. Говорят, что хранение стейта и виртуальный DOM – это чисто и просто, но это утверждение совсем неочевидно.
Состояние игры
Ванильный вариант начинается с объявления всех переменных, необходимых для отслеживания состояния игры. Если вас беспокоит большое количество глобальных данных, то во-первых код React тоже так делает, а во-вторых – просто оберните все это в IIFE.
Что касается истерических воплей о "побочных эффектах", то запомните уже: сделанное осознанно не является побочным эффектом. Доступ к глобальной области видимости – это не "чистое зло" как вам постоянно твердят. Но если это реально очень вас расстраивает, вы вольны потратить кучу времени, чтобы написать тот же код в ООП-стиле и везде рассовать свой любимый this
.
var lines = [ [ 0, 1, 2 ], [ 3, 4, 5 ], [ 6, 7, 8 ], [ 0, 3, 6 ], [ 1, 4, 7 ], [ 2, 5, 8 ], [ 0, 4, 8 ], [ 2, 4, 6 ] ], player, squares = make('fieldset', { repeat : [ 9, "input", { attr : { onclick : squareClick, type : "button" }, } ], attr : { id : "board" }, parent : document.body, }).elements, turn, turnHistory = [], turnOL = make("ol"), txtPlayer = new Text(), txtTurn = new Text(), winner;
Переменная squares
– это ссылка на нативную коллекцию fieldset#board.elements
, содержащую элементы игрового поля. Каждый элемент – простой input
, для которого вместо textContent
можно использовать value
. Чуть меньше кода, чуть легче манипуляции. Также устанавливаем обработчик кликов squareClick
– реализация будет чуть позже.
Назначение переменных player
, turn
, winner
, turnHistory
, turnOl
должно быть вполне очевидно. txtPlayer
и txtTurn
содержат ссылки на текстовые узлы. Обратите внимание, конструктор new Text()
– это новый document.createTextNode
.
Создаем дополнительный div
, в котором будет находиться вся информация о состоянии игры:
make('div', { append : [ txtTurn, " : ", txtPlayer, turnOL ], parent : document.body });
История ходов
Теперь делаем кнопки для перехода к каждому этапу игры напрямую. Простую функцию turnButton
можно вызывать после каждого хода, а также в начале игры для создания кнопки "Перейти к началу".
function turnButton(append, onclick, value) { make("li", { append : [ [ "button", { attr : { onclick, value }, append } ] ], parent : turnOL }); }
Каждая кнопка может хранить значение, и здесь очень удобно, что у элемента button
текст не связан с value
. Именуя аргументы в соответствии с названиями свойств и атрибутов, мы можем использовать краткий синтаксис создания объекта.
Функция restart
возвращает исходное состояние игры:
function restart() { for (var square of squares) square.value = ""; txtPlayer.textContent = player = "X"; winner = false; turn = 0; txtTurn.textContent = "Next Player"; }
- очищаем все ячейки игрового поля;
- устанавливаем активного игрока;
- обнуляем победителя и историю ходов.
Теперь создаем кнопку Вернуться к началу и инициализируем игру:
turnButton("Go To Game Start", restart); restart();
Обработка хода
Теперь нужен обработчик для кликов по сегментам игрового поля:
function squareClick(e) { e = e.currentTarget; if (winner || e.value) return; e.value = player; if (turnHistory.length > turn) { purge(turnOL, turnHistory.length - turn); turnHistory = turnHistory.slice(0, turn); } turnHistory.push(e); turn++; turnButton("Go to move " + turn, goToTurn, turn); calcWinner(); }
Просто берем элемент, на котором было вызвано событие, и смотрим, есть ли у него value
. Если нет, то сохраняем в него текущего игрока.
Если история ходов длиннее, чем индекс текущего хода (то есть пользователь решил "переходить"), то нужно стереть все, что было дальше, и записать новый ход.
Наконец проверяем, есть ли победитель. Вдруг после этого хода игроку удалось построить целую линию.
Проверка победителя
Функция для проверки очень простая и гораздо красивее, чем в React-варианте:
function calcWinner() { for (var [a, b, c] of lines) if ( (player == squares[a].value) && (player == squares[b].value) && (player == squares[c].value) ) { txtTurn.textContent = "Winner"; return txtPlayer.textContent = winner = player; } if (turn == 9) { txtTurn.textContent = "Tie"; txtPlayer.textContent = "Game Over"; } else nextPlayer(); }
- Массив
lines
не хранится внутри функции, а вынесен в глобальную область, поэтому его не нужно создавать каждый раз, тратя на это память. - Благодаря простому сравнению
value
поля с текущим игроком, условие получилось гораздо проще. - Цикл
for...of
позволяет выполнять деструктуризацию прямо цикле. - При необходимости мы возвращаем победителя, в противном случае он аннулируется по умолчанию. Вам не нужно явно возвращать
false
, перестаньте бороться с тем, что JS пытается сделать проще!
Также здесь проверяется последний ход и выполняется переход хода к следующему игроку.
Переходы по истории
Нам еще нужна функция для перехода к конкретному ходу.
В React-версии они на каждом ходу сохраняют все игровое поле. У нас другой подход – перезапуск игры с последовательным подключением выбранных полей.
function goToTurn(e) { restart(); for (var input of turnHistory) { input.value = player; if (++turn == e.currentTarget.value) break; nextPlayer(); } calcWinner(); }
Это работает намного быстрее, чем вся эта чепуха с состоянием, даже несмотря на то, что мы все сбрасываем и начинаем по сути сначала.
Переход хода
И наконец последняя функция:
function nextPlayer() { txtPlayer.textContent = player = player === "X" ? "O" : "X"; }
Не очень красивая, можно и почистить, но по сравнению с оригиналом вполне себе.
Сравнение
Размер оригинала 3247 байт, а ванильная версия весит 3354 байта. Однако не забывайте, что оригинал еще использует сам React.
Если убрать все комментарии и вспомогательные функции, получится 1956 байт, так что это абсолютная победа.
При этом в ванильной программе вы контролируете каждую строчку кода, а в оригинале основная функциональность библиотеки от вас скрыта.
Калькулятор температуры
Этот пример еще проще и еще менее продуман.
Здесь нам также потребуются вспомогательные "библиотечные" функции (кроме purge).
React-версия:
https://codepen.io/gaearon/pen/WZpxpz?editors=0010
Vanila-версия:
https://codepen.io/jason-knight/full/OJbGmoN
С точки зрения HTML тут сразу все плохо: они используют fieldset
и legend
для того, что должен делать label
. Ужасные люди!
Преобразования
Хуже того, они захаркодили преобразования вместо создания объекта с несколькими преобразованиями. Это тот самый случай, когда объекты и массивы делают код проще и эффективнее. Только посмотрите на это спагетти. Для каждого преобразования своя функция, обработчики и бесконечная цепочка мусора "функционального программирования", которая лишь добавляет накладные расходы. А ведь для всего этого хватило бы одного крошечного обработчика и одного ссылочного объекта!
var scales = { celcius : { fahrenheit : (t) => 32 + t * 1.8, kelvin : (t) => 273.15 + t }, fahrenheit : { celcius : (t) => (t - 32) / 1.8, kelvin : (t) => 273.15 + ((t - 32) / 1.8) }, kelvin : { celcius : (t) => t - 273.15, fahrenheit : (t) => (t - 273.15) * 1.8 + 32 } },
Сюда гораздо проще добавить новую температурную шкалу чем в оригинал, не так ли? Для примера мы добавили шкалу по Кельвину.
Представление
Для создания и оформления набора полей используем уже знакомую функцию make
:
root = make("fieldset", { append : [ [ "legend", [ "Enter a temperature in any field below for conversion" ] ] ], parent : document.body }), boilingText = new Text();
Здесь у нас fieldset
с правильной легендой, которая отражает реальный смысл всего этого элемента.
function makeTempInput(name, parent) { Object.defineProperty(scales[name], "input", { value : make("input", { attr : { name, oninput : onTempInput, pattern : "[-+]?[0-9]*[.,]?[0-9]+", type : "number" }, }) }); make("label", { append : [ name, ["br"], scales[name].input, ["br"] ], parent }); }
Мы используем Object.defineProperty
, чтобы сохранить ссылку на поле ввода в неперечисляемом свойстве объект шкалы.
В итоге получается вот такая разметка (хотя вы и сами уже должны были это понять):
<label> fahrenheit<br> <input id="temp_fahrenheit" name="fahrenheit" oninput="ontempinput();" pattern="[-+]?[0-9]*[.,]?[0-9]+" type="number" ><br> </label>
Этот фрагмент добавляется в корневой fieldset
, но ссылка на input
уже сохранена в объекте соответствующей шкалы.
for (var name in scales) makeTempInput(name, root);
Естественно, нам нужен обработчик события input
:
function onTempInput(event) { var input = event.currentTarget; for ( var [name, method] of Object.entries(scales[input.name]) ) scales[name].input.value = method(input.valueAsNumber); boilNoticeUpdate(); }
Получаем input, на котором было вызвано событие и определяем шкалу, к которой он привязан (input.name
). Для всех связанных с ней шкал выполняем преобразования и обновляем значения.
Обратите внимание на свойство valueAsNumber
– удобный способ сразу получить значение в виде числа.
В завершение проверяем, достигнута ли температура кипения воды:
function boilNoticeUpdate() { boilingText.textContent = scales.celcius.input.value >= 100 ? "" : "not"; }
Меняем лишь один крошечный textNode
, а не всю строку!
Вот в общем и все.
Сравнение
React-оригинал весит 2441 байт, а vanilla-версия 2630 байт.
Но опять же – у нас есть комментарии, вспомогательные функции и лишняя шкала по Кельвину.
Удалим это все и получим всего 1243 байта!
При этом код проще для понимания, легче масштабируется и более эффективен, потому что мы вырезали весь этот мусорный виртуальный DOM.
Методология React заставляет вас писать в два раза больше кода чем необходимо для решения задачи (это если не считать библиотечные функции). Это просто абстракция ради абстракции! Хватит уже верить в то, что фреймворки делают разработку лучше!