Нарушает ли React DOM-стандарты?

в 14:04, , рубрики: accessibility, javascript, React, ReactJS, ui library, Разработка веб-сайтов

Существует довольно популярный сайт https://custom-elements-everywhere.com где показывается как работают веб-компоненты в разных фреймворках. Почти у всех фреймворков там красивый 100% результат, но у React там очень настораживающие 71%:

Рейтинг React на custom-elements-everywhere.com
Рейтинг React на custom-elements-everywhere.com

Многие пользователи смотрят на эту страничку и делают вывод, что React плохо поддерживает не только веб-компоненты, но и DOM API в принципе. Так ли это? Действительно ли все плохо?

Давайте разбираться!

Анализируем тесты

Рейтинг высчитывается на основании тестов. Вот тут показывается результат. Всего 15 тестов, 7 из них сломаны, отсюда получаем такой неважный рейтинг. Сломаны следующие тесты:

  • attributes and properties

    • will pass array data as a property

    • will pass object data as a property

  • events

    • can declaratively listen to a lowercase DOM event dispatched by a Custom Element

    • can declaratively listen to a kebab-case DOM event dispatched by a Custom Element

    • can declaratively listen to a camelCase DOM event dispatched by a Custom Element

    • can declaratively listen to a CAPScase DOM event dispatched by a Custom Element

    • can declaratively listen to a PascalCase DOM event dispatched by a Custom Element

Можно заметить, что все это тесты с похожими именами, то есть они тестируют разные аспекты одной и той же штуки. Скорее всего все эти падения вызваны одной проблемой и можно исправить их все одним изменением. Поэтому цифра в 71% по сути ничего не значит, это могло быть и 90% и 15% в зависимости от фантазии автора, и сколько тест-кейсов он написал.

Однако факт остается, что-то не работает, и с этим нужно разбираться. Вся ситуация сводится к двум баг-репортам на Github:

Какие же сложности мешают "просто взять и починить" фреймворк? Давайте сначала разберемся с событиями.

События

В современных фреймворках принято добавлять обработчики событий сразу рядом со обычными атрибутами:

<input type="checkbox" checked={checked} onChange={handleChange} />

Это позволяет сразу видеть что подается на вход и выход элемента в одном месте. С веб-компонентами хотелось бы делать то же самое:

<custom-checkbox checked={checked} onChange={handleChange} />

Однако это не работает, потому что не каждый атрибут начинающийся с on* React превращает в обработчик события. Вместо этого они поддерживают избранный (пусть и довольно большой) список событий. Почему они так сделали, объясняет Sebastian Markbåge (один из разработчиков React) в этом комментарии. Вот мой перевод:

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

Конечно, мы можем добавить простую реализацию, которая оставляет эти вопросы открытыми. Однако, мы уже знаем, что даже если мы добавим такую простую модель, мы захотим задепрекейтить ее и удалить в будущем в пользу наших текущих изысканий. Нам кажется, что работа не стоит того ради небольшого синтаксического улучшения. Экосистема будет вынуждена поддерживать как ручные ссылки на DOM-элементы, _так и_ этот новый синтаксис, пока мы разрабатываем новую систему. Вместо этого вы можете уже сейчас пользоваться ручным управлением через ссылки на DOM-элементы (refs) и это будет рабочий вариант.

Таким образом, React не предоставляет прямого доступа к DOM-событиям, потому что они хотят их обернуть в свои абстракции, чтобы лучше поддержать будущие фичи React (вроде Concurrent mode), у разработчиков уже сейчас есть вариант добавлять события вручную через Refs API:

function CustomCheckbox({ checked, handleChange }) {
	const ref = useRef();

	useEffect(() => {
		ref.current.addEventListener("change", handleChange);
		return () => ref.current.removeEventListener("change", handleChange);
  }, [handleChange]);
  
	return <custom-checkbox ref={ref} />;
}

Если возможность добавления событий есть, то почему же тогда это не делается в тестах custom-elements-everywhere? Автор этой странички считает это хаком и настаивает на том, что поддержка должна быть встроена во фреймворк. Получается, что результаты этого теста основаны на субъективном мнении, а не технической возможности/невозможности решить задачу.

Свойства или атрибуты?

Вторая причина так называемой "несовместимости в веб-компонентами" это невозможность передать в них свойства. Все, что вы передаете веб-компоненту в JSX будет превращено в атрибут. Атрибуты могут быть только строками, поэтому передать сложный объект не получится (хаки с JSON.stringify) в расчет не берем:

<user-view user="{user}" />
<!-- рендерит <user-view user="[object Object]" /> -->

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

function UserView({ user }) {
	const ref = useRef();
	// обновлять свойство при новом объекте user
	useEffect(() => (ref.current.user = user), [user]);

	return <user-view ref={ref} />;
}

Более того, если написание обертки для каждого свойства и компонента может утомить, то можно взять вот этот маленький модуль, который научит JSX работать и со свойствами, и с событиями как надо:

/** @jsx h */
import { createElement } from "react";
import val from "@skatejs/val";

const h = val(createElement);

function Checkbox({ checked, handleChange }) {
	// работает!
	return <custom-checkbox checked={checked} onChange={handleChange} />;
}

Таким образом, технически задача решается, есть и переиспользуемый плагин, но Google Developer Advocates недовольны тем, что решение это не такое, как им бы хотелось. Ради этого они готовы дезинформировать публику о том, что у React есть проблемы с совместимостью с DOM (которых на самом деле нет).

Достоверность 100% рейтинга

Еще есть интересная ситуация со 100% результатами. Действительно ли это гарантия отсутствия проблем совместимости? Как бы не так!. Имена событий могут быть любой строкой и содержать любые символы (вы же всегда хотели сделать new CustomEvent('клик!'))?

Иногда это вызывает проблемы совместимости с синтаксисом шаблонов. Например, в Angular нельзя использовать двоеточие при назначении обработчика событий в шаблонах. При этом materials-components-web использует такую систему именования событий: MDCSlider:change. Возникает ироничная ситуация когда один проект Google (Angular) несовместим с другим проектом той же компании (Material design). Решение всё то же, нам уже знакомое – добавим обертку и назначим обработчик через прямое обращение к DOM-элементу.

Таким образом, необходимость создания оберток, из-за которой React влепили его 71% рейтинга, не помешала дать Angular 100%. Вот такой вот непредвзятый рейтинг.

На всякий случай замечу, что это не является проблемой ни одного из фреймворков. Имена свойств и событий могут быть самыми разными и не вписываться в синтаксис шаблонов. Явное лучше неявного, и брать ручное управление DOM в особых случаях – это нормально. Не очень понятно, какую цель преследует автор custom-elements-everywhere своим рейтингом.

Реальная ситуация

После того как мы разобрались c custom-elements-everywhere, давайте посмотрим на реальные проблемы совместимости между React и DOM API за пределами этой истории с веб-компонентами. Будем честны, они есть:

  1. onChange обработчик в React совсем не равен DOM-событию change. Это действительно проблема, причины такого поведения объясняются в этом [Github issue](https://github.com/facebook/react/issues/9657). Это вызывает сложности как при изучении React, так и при миграции с React на что-то другое, когда выясняется, что onChange в React – не настоящий.

  2. onFocus/onBlur события всплывают. В привычном нам DOM API, событие focus, вызывается только на том элементе, который получает фокус. В React же это событие всплывает по дереву компонентов, по сути работает как событие focusin. Больше об этом можно почитать в этом issue.

  3. События всплывают по порталам. Возможно, многие посчитают это фичей, но для полноты картины написать об этом стоит. Portal API позволяет рендерить элементами за пределами основного контейнера с React-приложением. При этом события продолжат всплывать по дереву компонентов а не DOM-дереву, как будто портала никакого и нет.

Список далеко не полный, но это те моменты с которыми довелось лично мне столкнуться и которые я бы хотел исправить. Как видно, все не так уж плохо, и несмотря на эти недостатки, с помощью React можно строить большие, но при этом доступные (семантичные) веб-интерфейсы и использовать полную мощь DOM API в тех местах, где React недостаточно (например, декларативно управлять фокусом через react-focus-lock).

Послесловие

Одним из фундаментальных требований по доступности к веб-сайтам является правильное оформление текстовых полей. Каждому полю должен быть назначен соответствующий label. Часто это делается через for и id атрибуты:

<label for="name">Ваше имя</label>
<input id="name" name="firstName" />

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

const id = useUniqueId();

<Label for={id}>Ваше имя</Label>
<Input id={id} name="firstName" />

При этом компоненты Label и Input рендерят соответствующие html-тэги и наше текстовое поле остается семантичным и доступным.

Попробуем сделать то же самое без React, но на веб-компонентах:

<custom-label for="name">Ваше имя</custom-label>
<custom-input id="name" name="firstName" />

Наше текстовое поле сломалось! Внутренний тэг label не смог связаться с тэгом input, потому что они находятся в разных ShadowDOM инстансах. Существует proposal призванный решить эту проблему, но он еще в зачаточной стадии и не работает ни в одном из браузеров (напоминаю, веб-компоненты разрабатываются без малого уже 10 лет). А в настоящий момент реализовать custom-label и custom-input в виде веб-компонентов, соблюдая требования доступности, не получится.

Вот и думайте сами, какая технология тут является настоящим нарушителем веб-стандартов.

Автор: Boris Serdiuk

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js