Web Components (Teil 2) – Einbindung in React

Im ersten Teil dieser Artikelreihe haben wir uns angeschaut, wie man eigene Web Components baut. Nun schauen wir uns die Einbindung in React-Anwendungen an.

Ihrer Idee nach sind Web Components unabhängig von JavaScript-Frameworks einsetzbar. Während dies beispielsweise bei Angular auch mit wenigen Handgriffen ohne Probleme funktioniert, sieht die Situation bei React leider etwas anders aus.​ Warum das so ist und wie man das Problem lösen kann, wird im Folgenden näher erläutert.

Prinzipiell lassen sich Web Components auch in React vollständig nutzen. Allerdings muss man für bestimmte Fälle zusätzlichen Aufwand betreiben und von üblichen React-Konventionen abweichen. Die Benutzung entspricht nicht mehr unbedingt dem, was React-Entwicklerinnen und -Entwickler erwarten würden.

Im Wesentlichen gibt es zwei Problemfelder: Einerseits handelt es sich dabei um das Problem „Attribute vs. Properties“, welchem wir uns in diesem Artikel widmen. Andererseits gibt es das Problem der „Custom-Events“ – dieses wird im nächsten Teil dieser Reihe behandelt.

Problembeschreibung „Attribute vs. Properties“

Wie wir im ersten Teil der Reihe gesehen haben, gibt es zwei Möglichkeiten, um Daten an eine Web Component zu übergeben – als HTML-Attribut oder als JavaScript-Property.

In diesem Code-Beispiel wird der „value“ als Attribut im HTML definiert:

<my-component value="something"></my-component>

Hier wird dagegen mittels JavaScript das gleichnamige Property gesetzt:

const myComponent = document.createElement("my-component")

myComponent.value = "something"

In JavaScript ist es aber auch möglich, explizit das Attribut zu setzen:

myComponent.setAttribute("value", "something")

JavaScript ist in dieser Hinsicht also flexibler, denn in HTML sind nur Attribute möglich – Properties lassen sich nicht in HTML setzen.

Wichtig zum Verständnis ist hierbei: Ob und wie Attribute und Properties von der Komponente verarbeitet bzw. berücksichtigt werden, liegt vollständig in der Implementierung der Komponente. Es gibt zwar die Best Practice, im Regelfall sowohl Attribute als auch Properties anzubieten und diese synchron zu halten, aber technisch ist niemand daran gebunden. Es wäre daher ohne Weiteres möglich, entweder nur Attribute oder nur Properties zu akzeptieren oder die beiden mit völlig unterschiedlichen Namen zu versehen (womit man aber sicherlich den Unmut der Benutzerinnen und Benutzer der Komponente auf sich ziehen würde).

Auf der anderen Seite gibt es jedoch auch handfeste Gründe, in manchen Fällen von dieser Best Practice bewusst abzuweichen.

Ein wichtiger Faktor ist, dass Attribute und Properties unterschiedlich mächtig sind: Attribute erlauben nur solche Werte, die als String repräsentiert werden können, d. h. Strings und Zahlen. Außerdem kann man durch die An- bzw. Abwesenheit eines Attributes auch Boolean-Werte abbilden. Komplexere Daten wie JavaScript-Objekte oder Funktionen können nicht als Attribut übergeben oder müssten serialisiert werden.

Bei JavaScript-Properties gibt es diese Beschränkung naturgemäß nicht. Allerdings haben Properties den Nachteil, dass sie in der Benutzung stets imperativ und nicht deklarativ sind. Anstatt  wie bei HTML einfach deklarativ zu sagen, welchen Zustand man haben möchte, muss man Properties mittels Befehlen imperativ der Reihe nachsetzen. Aus Entwicklersicht ist das eher unschön, denn besonders durch Frameworks wie React und (mit leichten Abstrichen) Angular hat man sich an die Vorzüge von deklarativem Arbeiten gewöhnt.

Ein weiterer Unterschied zwischen Attributen und Properties betrifft die Performance: Sowohl Attribute als auch Properties werden nicht nur dafür genutzt, Daten von außen in die Komponente zu geben, sondern auch, um auf Informationen der Komponente zugreifen zu können. Ein schönes Beispiel hierfür ist der Standard-HTML-Tag <video>, welcher die aktuelle Wiedergabeposition des abgespielten Videos mittels der JavaScript-Property „currentTime“ anbietet. Bei Abfrage dieser Properties erhält man die Position in Sekunden als Dezimalzahl. Ein dazu passendes HTML-Attribut existiert dagegen nicht. Ein solches Attribut müsste andernfalls ständig mit der aktuellen Abspielzeit aktualisiert werden, was im DOM eine relativ kostspielige Operation wäre. Die Abfrage über ein JavaScript-Property lässt sich dagegen recht performant lösen, da hierfür eine Lazy-Getter-Methode implementiert werden kann, die nur anspringt, wenn die Position tatsächlich abgefragt wird.

Wir haben bei Web Components somit zwei unterschiedliche Mechanismen für einen sehr ähnlichen Zweck, die sich dennoch in einigen Aspekten recht deutlich unterscheiden.

AttributeProperties
deklarativimperativ
String, Number, BooleanString, Number, Boolean, Date, Object, Function

React Props

Bei React sieht die Sache etwas übersichtlicher aus: React kennt lediglich so genannte „Props“. Da React einen starken Fokus auf deklaratives Programmieren legt, ähnelt die Benutzung der von HTML-Attributen:

<MyComponent value="something" />

React-Props sind aber nicht auf bestimmte Datentypen beschränkt, sondern erlauben das Übergeben von beliebigen Daten und auch von Funktionen. Hierfür wird anstelle der Anführungsstriche eine Syntax mit geschwungenen Klammern benutzt:

<MyComponent
    aDate={ new Date() }
    aNumber={ 12 }
    aComplexObject={ {firstname: "Hugo", lastname: "Meier" } }
    aFunction={ () => console.log("some action") }
/>

In gewisser Weise kombiniert React die jeweils positiven Aspekte von Attributen und Properties in einem einzigen Konzept. 

In der Komponente kommen die Daten in einem „props“-Objekt an, welches die übergebenen Werte als Key-Value-Paare enthält:

const MyComponent = (props) => {
    const aDate = props.aDate
    const aNumber = props.aNumber
    const aComplexObject = props.aComplexObject
    const aFunction = props.aFunction
    //...
}

Oder etwas kompakter mittels destructuring:

const MyComponent = ({ aDate, aNumber, aComplexObject, aFunction}) => {
    // ...
}

Als React-Entwickler muss ich sagen, dass mir persönlich die React-Variante mit Props deutlich besser gefällt als die Unterscheidung zwischen Attributen und Properties mit ihren jeweiligen Eigenarten bei Web Components – dies ist aber Geschmackssache.

Web Components in React

Nun ist die API von Web Components aber nun mal so, wie sie ist. Die Frage ist daher: Was passiert, wenn man eine Web Component in React benutzt? Werden „props“ als Attribute oder Properties an die Web Component weitergereicht?

Zunächst entscheidet React anhand der Groß- und Kleinschreibung des Tags, ob es sich um eine React-Komponente (beginnt mit Großbuchstaben) oder einen HTML-Tag handelt, worunter auch Web Components zählen. Mit Ausnahme einiger Sonderfälle bei einigen Standard-HTML-Tags setzt React Props bei HTML-Tags und Web Components immer mittels ​„setAttribute“​. Das heißt, dass die Benutzung von Attributen bei Web Components in React keine Probleme bereitet. Anders sieht es aus, wenn explizit JavaScript-Properties benutzt werden müssen, z. B. weil komplexe Daten oder Funktionen in die Web Component hineingereicht werden sollen. Dies lässt sich gegenwärtig mit React nicht deklarativ umsetzen. In gefühlt 90 % der Fälle stellt dies kein Problem dar, da es, wie oben bereits angemerkt, als Best Practice gilt, Attribute und Properties synchron zu halten und möglichst beide Varianten zu unterstützen. Nur in den restlichen 10 % der Fälle, in denen Properties notwendig sind, weil sich entweder die Autorinnen und Autoren der Web Component nicht an die Best Practice gehalten haben oder ein anderer Grund die Nutzung von Attributen verhindert, müssen wir uns etwas einfallen lassen.

Das heißt allerdings nicht, dass solche Web Components überhaupt nicht in React genutzt werden können! Wir können lediglich nicht den üblichen, rein deklarativen Weg gehen, sondern müssen auf die von React ebenfalls unterstützte, imperative API zurückgreifen. Wie dies funktioniert, wollen wir uns im Folgenden anschauen.

React abstrahiert von konkreten Instanzen von DOM-Knoten. Auch unabhängig von Web Components muss man aber in manchen Fällen direkt auf DOM-Knoten zugreifen, beispielsweise wenn eine Methode wie „.focus()“ aufgerufen werden soll. Für diesen Zweck nutzt React so genannte ​„Refs“​und genau diesen Mechanismus können wir auch für das Setzen von JavaScript-Properties an unseren Web Components benutzen. Im Code sieht das z. B. so aus:

import React, { useRef, useEffect } from "react"

const MyReactComponent = () => {
    const elementRef = useRef(null)

    useEffect(() => {
        if(elementRef.current) {
            elementRef.current.someProperty = "value"
        }
    }, [elementRef])

    return <my-custom-element ref={elementRef} />
}

Mit „const elementRef = useRef(null)“ erstellen wir eine Art Container, in die React nach dem Rendern die Referenz zum DOM-Knoten packt. „useEffect​“ kann dazu genutzt werden, eine Funktion auszuführen, sobald bestimmte Variablen verändert wurden. Dazu geben wir die „elementRef​„-Variable (in ein Array gewrappt) als zweiten Parameter an die​ „useEffect„-Hook-Funktion. Sobald React die Komponente das erste Mal gerendert hat, wird​ die angegebene Funktion ausgeführt, so dass unser Property entsprechend gesetzt wird. Wie man sieht, ist der Code doch um einiges umständlicher als lediglich ein Attribut direkt am Tag zu setzen. Das Beispiel zeigt aber, dass es eben doch möglich ist, Web Components in React zu nutzen. Im vierten Teil dieser Artikelreihe werden wir uns noch eine andere Variante anschauen, die besonders bei größeren Anwendungen, in denen bestimmte Web Components immer wieder eingesetzt werden sollen, besser skaliert. Im nächsten Artikel der Reihe schauen wir uns aber zunächst das zweite Problem von Web Components mit React genauer an: Die Verarbeitung von Custom-Events.

Fazit

Als Zwischenfazit lässt sich feststellen, dass die Situation von Web Components mit React kompliziert ist. Auf der einen Seite ist React hervorragend für die Entwicklung von umfangreichen Web-Anwendungen geeignet und daher auch weit verbreitet. Auf der anderen Seite ist es äußerst ärgerlich, dass React bei einer modernen Web-Technologie wie Web Components solche Probleme hat.

Als Grund hierfür lassen sich mindestens zwei Faktoren nennen: Zum einen entstand React zu einer Zeit, in der Web Components bzw. „custom elements“ noch eine bloße Idee und weit von der praktischen Umsetzung entfernt waren. Gleichzeitig legt das React-Team großen Wert auf Abwärtskompatibilität und schreckt verständlicherweise vor inkompatiblen Änderungen in der Art und Weise, wie React-Komponenten geschrieben werden, zurück. Die Diskussion dazu, welche Optionen im Raum stehen, um React kompatibel zu Web Components zu machen, kann bei Interesse im Issue-Tracker des Projekts​ verfolgt werden.

Der zweite Faktor, den ich hervorheben möchte, ist: Die Konzepte von Web Components und React unterscheiden sich relativ stark voneinander, wenn es darum geht, wie Komponenten benutzt werden. React ist vollständig auf deklaratives Programmieren ausgelegt, während Web Components und auch Standard-HTML-Tags eine Mischform vorsehen, die teilweise deklarativ, an einigen Stellen aber eben auch zwingend imperativ ist. Und da React-Entwicklerinnen und -entwickler genau diesen deklarativen Charakter von React mögen, kann es nicht die Lösung sein, die imperative API von Web Components einfach blind zu übernehmen. Stattdessen müssen Wege gefunden werden, wie eine Zusammenarbeit zwischen diesen beiden „Welten“ möglich ist. Leider dauert der Prozess dieser Lösungssuche mittlerweile schon relativ lange an und zwischenzeitlich schien die Diskussion innerhalb der Community der React-Entwicklerinnen und -Entwickler ein bisschen eingeschlafen zu sein.

Es bleibt daher nur zu hoffen, dass dieser Prozess wieder Fahrt aufnimmt, so dass Web Components in Zukunft auch in React-Projekten einfach und ohne umständliche Umwege eingesetzt werden können. 

Dieser Beitrag wurde verfasst von: