Более простой способ сделать императивный код в приложениях React.
Независимо от того, насколько хорошо мы пишем приложения React, мы не можем полностью избежать кодирования императивных интерфейсов. Это связано с тем, что пользовательский интерфейс принципиально императивен, как и API в браузере.
Управление медиа-элементами является распространенным случаем, но мы также можем иметь дело со сторонними библиотеками, которые верны соглашениям внутри браузеров. Декларативная модель React плохо сочетается с императивными API, и нам нужно каким-то образом преодолеть разрыв между «что» и «как». В этой статье мы обсудим один из таких шаблонов.
Люди, которые следят за мной, знают, что я твердо верю в ванильный JavaScript. Важный урок, который я вынес из ванильных приключений, заключается в том, что у каждого решения есть естественное место, где оно должно быть реализовано, и отклонение от этого места создает шаблон и сложность. Если вы когда-либо чувствовали, что использование useEffect
и useRef
, а также ручное отслеживание зависимостей ловушек кажется немного странным по сравнению с обновлениями пользовательского интерфейса на основе состояния, это потому, что это так. Это не соответствует этой модели (каламбур) и не принадлежит реактивному пользовательскому интерфейсу.
Шаблон декларативно-императивного моста призван решить эту проблему и поместить мост между декларативным и императивным юниверсами в более естественное место. Под естественным местом я подразумеваю место, где уже существует четко определенный механизм, который делает именно то, что нам нужно. В случае декларативно-императивного соединения естественным образом это происходит в механизме наблюдения за атрибутами в пользовательских элементах.
Это относительно новый шаблон, который мы могли реально использовать в наших целевых браузерах только примерно с 2020 года, когда спецификация пользовательских элементов стала поддерживаться во всех браузерах после того, как Microsoft окончательно отказалась от своего старого браузера Edge и переключилась на браузер на основе Chromium. . Safari по-прежнему не поддерживает все спецификации (спасибо, Apple!), но это не имеет отношения к тому, что мы собираемся здесь сделать.
Сторона React
(Для полноты вставьте сюда старый добрый пример со ссылками и эффектами.)
Я предполагаю, что вы знакомы с обычным способом сделать это, используя ссылки и эффекты. Я не буду утомлять вас примерами этого паттерна. Давайте погрузимся прямо в DIB.
Часть React в значительной степени остается такой же, как и любой реактивный компонент, управляемый состоянием.
Например:
let STOPPED = 0, PLAYING = 1 let Player = () => { let [playbackState, setPlaybackSate] = useState(STOPPED), onPlay = () => setPlaybackState(PLAYING), onStop = () => setPlaybackState(STOPPED) return ( <article> <react-audio playback-state={playbackState}> <audio src="foo.mp3"></audio> </react-audio> <button onClick={onPlay} aria-pressed={playbackState === PLAYING}> Play </button> <button onClick={onStop}> Stop </button> </article> ) }
Я надеюсь, что мне не нужно объяснять, что делает приведенный выше код, потому что это своего рода смысл шаблона DIB — простота.
Однако отмечу одну маленькую деталь. Элемент <audio>
является дочерним элементом пользовательского элемента <react-audio>
. Это важно, поскольку React имеет полный контроль над поддеревом пользовательского элемента. В примере кода именно React указывает источник и фактически создает элемент <audio>
. Если бы мы хотели получать уведомления о текущем времени во время воспроизведения, мы могли бы прикрепить обработчик событий onTimeUpdate
непосредственно к элементу <audio>
, а не к пользовательскому элементу.
Мост
Давайте взглянем на пользовательский элемент, где происходит фактическое декларативно-императивное соединение. Мы называем этот элемент элементом «мост».
customElements.define('react-audio', class extends HTMLElement { static get observedAttributes() { return ['playback-state'] } attributeChangedCallback() { if (this.getAttribute('playback-state') == STOPPED) { this.firstElementChild.pause() this.firstElementChild.currentTime = 0 } else { this.firstElementChild.play() } } })
Пользовательский элемент объявляет атрибут playback-state
наблюдаемым. Это приведет к вызову метода экземпляра attributeChangedCallback()
при каждом изменении атрибута.
В attributeChangedCallbak()
мы переводим декларативную концепцию состояния воспроизведения в императивное действие, выполняемое над элементом <audio>
, который является первым (и единственным) дочерним элементом пользовательского элемента.
Имейте в виду, что это может усложняться по мере роста наших требований. У нас может быть несколько наблюдаемых атрибутов, мы можем генерировать события и т. д. Говоря о событиях, обратите внимание, что из-за реализации синтетических событий React ограничен несколькими стандартными событиями. Мы не можем использовать пользовательские события.
Элементу <react-audio>
не нужно создавать дочерние узлы, так как это не является его целью. Поскольку мы работаем с пользовательскими элементами, у нас по-прежнему есть возможность рендеринга всего поддерева, даже используя для этой цели теневой DOM. Поступая таким образом, мы отказываемся от некоторого контроля над дочерними узлами со стороны React, но открываем двери для некоторых других вещей (например, повторного использования кода в приложении с использованием разных фреймворков или даже без фреймворков). Дело в том, что визуализируете ли вы поддерево в React или в пользовательском элементе, это не имеет отношения к шаблону DIB, и шаблон не заставляет вас делать это так или иначе.
Но, но… пользовательские элементы — это не React!
Конечно, нет. В этом весь смысл!
Весь смысл этого паттерна в том, чтобы переместить императивный — и, следовательно, не специфичный для React — код из хуков эффектов и из компонентов, где он все равно не совсем подходит. Это код, который вам все равно нужно было бы написать где-нибудь, но если его завернуть в хук, он не станет более «реактивным» и «управляемым состоянием», чем рулон туалетной бумаги. Я вижу это как «чужой» код, встроенный в компонент React, очень похожий на дикое животное, забредшее на чей-то задний двор. Его спасают и переводят в естественную среду обитания.
Краткое содержание
Напомним, шаблон DIB работает следующим образом:
- React делает то, что React делает лучше всего — изменения пользовательского интерфейса, управляемые состоянием — мы просто избегаем императивного кода (эффектов) в компонентах.
- Разрыв между декларативными и императивными API устраняется функцией наблюдаемых атрибутов пользовательских элементов.
- Весь императивный код находится внутри пользовательского элемента
- Пользовательский элемент может использовать или не использовать поддерево, созданное React, он не имеет отношения к шаблону.
Вы найдете полностью рабочий исходный код более полной реализации DIB в моем репозитории GitHub.