Startseite / Artikel

Vom JSX zu einfachen Objekten: Wie React-Elemente, -Komponenten und -Props funktionieren.

Erfahren Sie, was aus JSX kompiliert wird, warum Komponenten einfach Funktionen sind und wie Props, Kinderelemente, Standardwerte, Spread-Operationen sowie Slots aus diesem einen Denkmuster ableiten.

Verlag: REACTAPP.TOP9 Min. Lesezeit1983 Wörter

Quellen: JSX, Components & Props — The Mental Model Every React Developer Actually Needs

Viele React-Bugs entstehen durch ein unklares Verständnis davon, was JSX, Komponenten und Props eigentlich sind. Sobald man weiß, dass JSX zu einer Funktionsaufruf wird, dieser Aufruf ein einfaches Objekt erzeugt und eine Komponente eine Funktion ist, die ein solches Argument-Objekt entgegennimmt, fangen Regeln, die anfangs willkürlich erschienen, an, Sinn zu ergeben. Dieser Artikel entwickelt dieses Modell von der Ausgabe des Compilers ausgehend und wendet es anschließend auf Props, children, Standardwerte, das Ausbreiten von Eigenschaften, Immutabilität sowie Kompositionsmustern an, die man täglich verwendet.

JSX sind Funktionsaufrufe, keine HTML

JSX wird oft als „HTML innerhalb von JavaScript“ bezeichnet, was unvollständig ist. Genauer gesagt handelt es sich bei JSX um syntaktischen Zuckerguss, den ein Compiler in gewöhnliche Funktionsaufrufe umwandelt.

Nehmen wir einen einfachen Überschriftstext:

const element = <h1 className="title">Hello World</h1>;

Ein Compiler wie Babel wandelt dies in einen Aufruf von React.createElement um, wobei der Tagname, ein Objekt mit Attributen sowie der Inhalt übergeben werden:

const element = React.createElement("h1", { className: "title" }, "Hello World");

Das ist die klassische Transformation. Seit React 17 verwenden die meisten Toolchains den automatischen JSX-Runtime, der stattdessen Aufrufe einer aus react/jsx-runtime importierten jsx()-Funktion auslöst, sodass React nicht mehr im Scope benötigt wird. Die Idee bleibt dieselbe: Das Markup wird zu einem Funktionsaufruf.

Der Aufruf gibt ein einfaches JavaScript-Objekt zurück, das als React-Element bezeichnet wird:

{
  type: "h1",
  props: {
    className: "title",
    children: "Hello World"
  }
}

Ein React-Element ist nichts anderes als eine Beschreibung dessen, was auf dem Bildschirm angezeigt werden soll. React durchläuft den Baum dieser Objekte und ermittelt, wie der eigentliche DOM damit in Einklang gebracht werden kann; dieser Baum von Beschreibungen ist das, was gemeinhin als Virtueller DOM bezeichnet wird.

Was das Modell erklärt

Die Erinnerung daran, dass „JSX zu einem Funktionsaufruf wird, der ein Objekt zurückgibt“, klärt mehrere häufige Fragen:

  • Keine if-Anweisungen innerhalb von JSX. Man kann keine Anweisung in einen Funktionsparameter einfügen, daher verwendet man eine ternäre oder &&-Ausdruck oder verschiebt die Logik oberhalb von return.
  • className anstelle von class. JSX ist JavaScript, und class ist dort ein reserviertes Wort.
  • Ein einzelnes Wurzelelement. Eine Funktion gibt einen Wert zurück, daher muss eine Komponente ein Element zurückgeben (das viele Kinder enthalten kann).

Eine Komponente ist eine Funktion, die Elemente zurückgibt

Komponenten sind Funktionen, deren Rückgabewert ein React-Element oder null ist, um nichts anzuzeigen.

function Greeting() {
  return <h1>Hello, Devendra!</h1>;
}

Es gibt keine versteckten Mechanismen, und in modernem React sind Klassen nicht notwendig. React ruft die Funktion auf, sobald es wissen muss, was dargestellt werden soll. Wenn sich die Daten ändern, ruft React sie erneut auf, erhält einen neuen Elementbaum, vergleicht ihn mit dem vorherigen und aktualisiert nur die Teile des DOMs, die sich unterscheiden. Dieser Vergleich wird als Reconciliation bezeichnet.

Zwei Regeln, die jede Komponente befolgen muss

Die Namen müssen mit einem Großbuchstaben beginnen

JSX verwendet die Großschreibung, um zu bestimmen, was eine Schriftart bedeutet. Eine mit Großbuchstaben geschriebene Schriftart bezieht sich auf eine Komponente, wodurch React Ihre Funktion aufruft:

// ✅ React treats this as a component
<Greeting />

Eine in Kleinbuchstaben geschriebene Schriftart wird als eingebautes DOM-Element behandelt, weshalb React versuchen würde, ein echtes <greeting>-Element zu erstellen, anstatt irgendetwas aufzurufen:

// ❌ React treats this as a native DOM element (like <div>)
<greeting />

Geben Sie genau einen Wurzelelement zurück

Es fehlt bei dem Versuch, zwei sibling-Elemente nebeneinander zurückzugeben, weil dadurch zwei Werte aus einer Funktion zurückgegeben werden:

// ❌ This will throw an error
function Card() {
  return (
    <h2>Title</h2>
    <p>Description</p>
  );
}

Es gibt zwei Lösungen. Umhüllen Sie die sibling-Elemente in ein Elternelement oder verwenden Sie ein Fragment, das mit <>...</> geschrieben wird und sie gruppiert, ohne einen zusätzlichen Knoten im DOM hinzuzufügen. Der folgende Auszug zeigt beide Versionen nacheinander.

// ✅ Wrap in a parent
function Card() {
  return (
    <div>
      <h2>Title</h2>
      <p>Description</p>
    </div>
  );
}// ✅ Or use a Fragment (renders nothing extra in the DOM)
function Card() {
  return (
    <>
      <h2>Title</h2>
      <p>Description</p>
    </>
  );
}

Wählen Sie das Fragment, es sei denn, der Wrapper ist für Styling oder Layout erforderlich.

Props sind die Argumente der Funktion

Falls Komponenten Funktionen sind, sind Props ihre Argumente. Hier liest die Komponente einen name-Wert aus dem Objekt, das sie erhält:

function Greeting(props) {
  return <h1>Hello, {props.name}!</h1>;
}

Und hier wird sie mit diesem gesetzten Attribut dargestellt:

// Usage
<Greeting name="Devendra" />

React sammelt alle in der JSX-Tag geschriebenen Attribute in ein Objekt namens props und übergeben dieses als erstes Argument. Das ist das gesamte Prinzip.

Dekonstruktion in der Signatur

Das wiederholte Verwenden von props. wird schnell unübersichtlich. Durch Dekonstruktion des Objekts in der Parameterliste erhält jedes verwendete Prop einen eigenen Namen und die Eingabeparameter der Komponente werden auf einen Blick dokumentiert:

function Greeting({ name, role }) {
  return (
    <div>
      <h1>Hello, {name}!</h1>
      <p>Role: {role}</p>
    </div>
  );
}

Der Aufrufort bleibt unverändert:

<Greeting name="Devendra" role="Senior Engineer" />

Standardwerte mit Dekonstruktion

Falls ein Prop optional ist, geben Sie ihm direkt im Dekonstruktionsmuster einen Standardwert. Wenn role nicht angegeben wird, verwendet die Komponente standardmäßig "Developer":

function Greeting({ name, role = "Developer" }) {
  return (
    <div>
      <h1>Hello, {name}!</h1>
      <p>Role: {role}</p>
    </div>
  );
}

Durch diese Verwendung wird der Standardwert für role angezeigt:

// role will be "Developer" if not provided
<Greeting name="Devendra" />

Destructuring-Defaults gelten nur dann, wenn der Wert undefined ist. Das explizite Übergeben von null umgeht den Default – das sollte man im Hinterkopf behalten, wenn Props aus API-Daten stammen.

children: Komposition, die in der Syntax integriert ist

Der zwischen den Öffnungs- und Schließungstags einer Komponente eingebettete Inhalt wird automatisch als children-Prop übergeben. Eine Card kann einfach alles anzeigen, was ihr innerhalb ihres Wrappers gegeben wird:

function Card({ children }) {
  return (
    <div className="card">
      {children}
    </div>
  );
}

Der Aufrufer entscheidet, was hineingefügt wird:

// Usage
<Card>
  <h2>My Title</h2>
  <p>Some description here.</p>
</Card>

Durch children wird die Komposition realisiert, wodurch flexible, wiederverwendbare Komponenten erstellt werden. Anstatt jede Variante mit einem weiteren Prop vorzusehen, bietet die Komponente die Struktur und der Elternteil liefert den Inhalt. Bibliotheken wie shadcn/ui und Material UI bauen Dialoge, Karten und Schaltflächen auf diese Weise.

Jeder JavaScript-Wert kann ein Prop sein

Klammern in JSX bedeuten „bewerte dies als JavaScript“, sodass ein Prop jeden Wert enthalten kann: Zeichenketten, Zahlen, Boolesche Werte, Arrays, Objekte oder Funktionen. Dieses Komponente erhält mehrere dieser Werte gleichzeitig, rendernt ein Badge bedingungsweise, wandelt ein Array in Listeinträge um und verbindet eine Callback-Funktion mit einem Button:

function UserCard({ name, age, isAdmin, hobbies, onDelete }) {
  return (
    <div>
      <h2>{name}</h2>
      <p>Age: {age}</p>
      {isAdmin && <span>Admin</span>}
      <ul>
        {hobbies.map(h => <li key={h}>{h}</li>)}
      </ul>
      <button onClick={onDelete}>Delete</button>
    </div>
  );
}

Der Aufrufort zeigt, wie jede Art von Wert übergeben wird:

<UserCard
  name="Devendra"
  age={28}
  isAdmin={true}
  hobbies={["coding", "teaching", "gaming"]}
  onDelete={() => console.log("deleted")}
/>

Wichtige Hinweise:

  • Einfache Zeichenkettenwerte können direkt in Anführungszeichen geschrieben werden.
  • Alles andere, einschließlich Zahlen, Boolescher Werte, Arrays, Objekten und Funktionen, muss in Klammern stehen.
  • Eine Funktionseigenschaft wie onDelete dient dazu, dass ein Kind Ereignisse an seinen Elternteil zurückmeldet. Dieses Callback-Muster macht es möglich, den Zustand zentral zu verwalten: Der Zustand befindet sich im gemeinsamen Elternteil, und die Kinder ändern ihn nur, indem sie Funktionen aufrufen, die der Elternteil bereitstellt.
  • Eigenschaften sind nur zum Lesen zugänglich

    Verändern Sie Eigenschaften niemals. Wenn man einem Feld eines Eigenschaftsobjekts einen Wert zuweist, ändert sich damit heimlich die vom Elternteil verwaltete Datenmenge:

    // ❌ Never do this
    function Greeting({ user }) {
      user.name = "Modified"; // This will cause silent bugs
      return <h1>{user.name}</h1>;
    }
    

    Erstellen Sie stattdessen einen neuen Wert und lassen Sie den ursprünglichen unverändert:

    // ✅ Create a new value instead
    function Greeting({ user }) {
      const displayName = user.name.toUpperCase();
      return <h1>{displayName}</h1>;
    }
    

    Die Daten fließen in eine Richtung, vom Elternteil zum Kind – genau das sorgt dafür, dass React-Anwendungen vorhersehbar bleiben. Ein Kind, das möchte, dass der Elternteil etwas ändert, ruft eine von diesem bereitgestellte Funktion auf.

    Vermittlung von Eigenschaften mit rest und spread

    Wenn ein Komponente ein DOM-Element umschließt, möchte man oft viele Attribute akzeptieren, ohne jedes einzeln aufzulisten. Die Restsyntax im Parameter extrahiert die Eigenschaften, die die Komponente selbst verarbeitet – hier label – und sammelt den Rest in inputProps, der anschließend auf das input übertragen wird:

    function Input({ label, ...inputProps }) {
      return (
        <div>
          <label>{label}</label>
          <input {...inputProps} />
        </div>
      );
    }
    

    Jedes zusätzliche Attribut, wie type, placeholder oder onChange, wird direkt an das zugrunde liegende Element weitergeleitet:

    // All extra props (type, placeholder, onChange, etc.) flow straight to <input>
    <Input
      label="Email"
      type="email"
      placeholder="you@example.com"
      onChange={handleChange}
    />
    

    Libraryn verwenden dieses Muster ständig, doch das Hinzufügen willkürlicher Eigenschaften an ein DOM-Element kann Werte weiterleiten, die es nicht versteht, wodurch Warnungen wegen unbekannter Attribute ausgelöst werden. Seien Sie sorgfältig damit, was und wo Sie hinzufügen. Das Beispiel zeigt außerdem ein label, das nicht mit dem Eingabefeld verknüpft ist; in echtem Code sollten sie mithilfe von htmlFor und einem id verbunden werden, damit das Label zugänglich ist.

    Drei Ebenen der Komponentenflexibilität

    Komponenten entwickeln sich im Allgemeinen durch erkennbare Phasen, je nachdem wie die Anforderungen wachsen.

    Ebene 1: eine Eigenschaft für jede Variante

    Die einfachste Schaltfläche akzeptiert eine separate Eigenschaft für jedes Element, das sie anzeigen könnte:

    function Button({ text, color, icon }) {
      return <button style={{ color }}>{icon} {text}</button>;
    }
    

    Jede neue Variante erfordert wieder ein weiteres Attribut, wodurch die API unbegrenzt wächst. Dieses Problem wird oft als „Prop-Explosion“ bezeichnet; es unterscheidet sich von der sogenannten „Prop-Drilling“-Methode, bei der Daten über viele Ebenen weitergeleitet werden.

    Niveau 2: Kombination von Kind-Komponenten

    Durch die Erlaubnis für Aufrufer, Inhalte als children zu übergeben, entfallen die meisten dieser Attribute. Der Button behält nur ein variant-Attribut zum Styling bei:

    function Button({ children, variant }) {
      return <button className={`btn btn-${variant}`}>{children}</button>;
    }
    

    Der Aufrufer kann nun bestimmen, was innerhalb des Elements angezeigt wird – einschließlich Icons:

    <Button variant="primary">
      <Icon name="save" /> Save Changes
    </Button>
    

    Niveau 3: Benannte Slots

    Falls eine Komponente mehrere unterschiedliche Bereiche benötigt, können JSX-Daten über benannte Attribute übergeben werden. Eine Seitenstruktur kann beispielsweise einen Header, eine Seitenspalte und Hauptinhalt enthalten:

    function PageLayout({ header, sidebar, children }) {
      return (
        <div className="layout">
          <header>{header}</header>
          <aside>{sidebar}</aside>
          <main>{children}</main>
        </div>
      );
    }
    

    Jeder Bereich wird an der Stelle des Aufrufs mit Inhalten gefüllt:

    <PageLayout
      header={<Navbar />}
      sidebar={<FilterPanel />}
    >
      <ProductList />
    </PageLayout>
    

    Slots sind gewöhnliche Eigenschaften, die JSX enthalten: Die Aufrufer bestimmen, was jede Zone füllt, die Komponente steuert die Struktur, und das Layout bleibt auch bei sich ändernden Anforderungen erhalten. Für eine detailliertere Betrachtung dieses Refactoring-Ansatzes siehe die Behebung der React-Prop-Überlastung mit Composition und Slots.

    Kurzfassung des Modells

    • JSX: syntaktischer „Zucker“ für Funktionsaufrufe zur Erstellung von Elementen wie React.createElement().
    • React-Element: ein einfaches JavaScript-Objekt, das die Benutzeroberfläche beschreibt.
    • Komponente: eine Funktion, die React-Elemente zurückgibt.
    • Eigenschaften: das einzige Objekt mit Argumenten, das an diese Funktion übergeben wird.
    • children: eine spezielle Eigenschaft, die eingebetteten JSX-Inhalt enthält.
  • Prop-Mutation: niemals zulässig; betrachten Sie Props als nur zum Lesen zugänglich.
  • Pureness ist die Regel, die skalierbar macht

    Alles hängt von der Pureness ab: Die gleichen Props sollten immer denselben Ausgabewert liefern. Wenn das Rendering keine Nebeneffekte hat, Props niemals verändert und den externen Zustand nicht modifiziert, kann React Arbeitsschritte sicher überspringen, wiederholen oder umordnen, und Sie können jede Komponente isoliert betrachten. Im Strict Mode werden in der Entwicklung sogar Komponenten zweimal gerendert, um unreine Logik aufzudecken.

    Verletzen Sie die Pureness, ziehen Sie Fehler nach sich, die weit von ihrer Ursache entfernt auftreten. Bewahren Sie sie bei – dann bleiben Ihre Komponenten vorhersehbar, leicht testbar und wartbar, auch wenn die Anwendung wächst.