19 апреля 2021

⚛ Реакт – хлам, и я вам это докажу!

Frontend-разработчик в Foquz. https://www.cat-in-web.ru/
Современные фронтенд-фреймворки обещают вам быструю разработку, простую интеграцию и избавление от всех возможных проблем. На самом деле обычно вы получаете совсем другое.
⚛ Реакт – хлам, и я вам это докажу!

Статья публикуется в переводе, автор оригинального текста Джейсон Найт.

Во всех этих ваших модных React, Vue и Angular нет никакого смысла. На стороне сервера они не делают ничего такого, с чем не могли бы справиться шаблонные строки – причем гораздо чище и эффективнее. Как будто HTML для вас слишком сложен, и вы решили усложнить его еще больше.

На клиентской стороне фреймворки разрушают юзабельность и доступность, так как многие важные вещи (вроде корзины покупок) просто не могут работать без 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

Проблемы оригинала – отсутствие ясности кода и нерациональное использование DOM. Часть этих проблем не связана с JavaScript, но явно говорит о профессиональном уровне разработчиков подобных систем.

Например, вместо бессмысленного 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;

    
Объявление всех переменных в одном месте очень удобно для работы. Гораздо удобнее, чем их разбрасывание по всему коду. Pascal/Modula/Ada в этом плане просто молодцы.

Переменная 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 байт, так что это абсолютная победа.

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

React vs Vanilla
React vs Vanilla

Калькулятор температуры

Этот пример еще проще и еще менее продуман.

Здесь нам также потребуются вспомогательные "библиотечные" функции (кроме purge).

React-версия:

https://codepen.io/gaearon/pen/WZpxpz?editors=0010

Vanila-версия:

https://codepen.io/jason-knight/full/OJbGmoN

С точки зрения HTML тут сразу все плохо: они используют fieldset и legend для того, что должен делать label. Ужасные люди!

Весь этот мусорный JSX только раздувает ваш код и делает вещи сложнее, а не проще!

Преобразования

Хуже того, они захаркодили преобразования вместо создания объекта с несколькими преобразованиями. Это тот самый случай, когда объекты и массивы делают код проще и эффективнее. Только посмотрите на это спагетти. Для каждого преобразования своя функция, обработчики и бесконечная цепочка мусора "функционального программирования", которая лишь добавляет накладные расходы. А ведь для всего этого хватило бы одного крошечного обработчика и одного ссылочного объекта!

        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 vs. Vanilla
React vs. Vanilla
***

Методология React заставляет вас писать в два раза больше кода чем необходимо для решения задачи (это если не считать библиотечные функции). Это просто абстракция ради абстракции! Хватит уже верить в то, что фреймворки делают разработку лучше!

Продолжение следует...

Источники

МЕРОПРИЯТИЯ

Комментарии

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ