Svelte für erfahrene React Entwickler

Avatar of Adam Rackis
Adam Rackis am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

Dieser Beitrag ist eine beschleunigte Einführung in Svelte aus der Sicht von jemandem mit fundierter Erfahrung mit React. Ich gebe eine kurze Einführung und konzentriere mich dann unter anderem auf Themen wie State Management und DOM-Interoperabilität. Ich plane, recht zügig voranzugehen, um viele Themen abzudecken. Am Ende hoffe ich vor allem, ein gewisses Interesse an Svelte zu wecken.

Für eine geradlinige Einführung in Svelte kann kein Blogbeitrag jemals das offizielle Tutorial oder die Dokumentation übertreffen.

„Hallo, Welt!“ im Svelte-Stil

Beginnen wir mit einer kurzen Tour, wie eine Svelte-Komponente aussieht.

<script>
  let number = 0;
</script>

<style>
  h1 {
    color: blue;
  }
</style>

<h1>Value: {number}</h1>

<button on:click={() => number++}>Increment</button>
<button on:click={() => number--}>Decrement</button> 

Dieser Inhalt kommt in eine .svelte-Datei und wird vom Rollup- oder webpack-Plugin verarbeitet, um eine Svelte-Komponente zu erzeugen. Hier gibt es ein paar Teile. Gehen wir sie durch.

Zuerst fügen wir ein <script>-Tag mit dem benötigten Zustand hinzu.

Wir können auch ein <style>-Tag mit beliebiger CSS hinzufügen. Diese Stile sind **komponentengebunden (scoped)**, so dass hier <h1>-Elemente in *dieser* Komponente blau sein werden. Ja, gescoptes CSS ist in Svelte integriert, ohne dass externe Bibliotheken benötigt werden. Mit React müssten Sie normalerweise eine Drittanbieterlösung verwenden, um gescoptes Styling zu erreichen, wie z. B. css-modules, styled-components oder ähnliches (es gibt Dutzende, wenn nicht Hunderte von Auswahlmöglichkeiten).

Dann gibt es das HTML-Markup. Wie zu erwarten, gibt es einige HTML-Bindings, die Sie lernen müssen, wie {#if}, {#each} usw. Diese domänenspezifischen Sprachmerkmale mögen im Vergleich zu React, wo alles „nur JavaScript“ ist, ein Rückschritt sein. Aber es gibt ein paar Dinge, die beachtenswert sind: Svelte erlaubt es Ihnen, beliebiges JavaScript *innerhalb* dieser Bindings zu platzieren. So etwas wie das hier ist völlig gültig

{#if childSubjects?.length}

Wenn Sie von Knockout oder Ember zu React gewechselt sind und nie zurückgeblickt haben, könnte dies eine (angenehme) Überraschung für Sie sein.

Auch die Art und Weise, wie Svelte seine Komponenten verarbeitet, unterscheidet sich stark von React. React führt alle Komponenten immer wieder aus, sobald sich ein beliebiger Zustand innerhalb einer Komponente oder irgendwo in einer übergeordneten Komponente ändert (es sei denn, Sie „memoizen“). Dies kann ineffizient werden, weshalb React Dinge wie useCallback und useMemo mitliefert, um unnötige Neuberechnungen von Daten zu verhindern.

Svelte hingegen analysiert Ihr Template und erstellt gezielten DOM-Update-Code, wann immer sich ein *relevanter* Zustand ändert. In der obigen Komponente sieht Svelte die Stellen, an denen sich number ändert, und fügt Code hinzu, um den <h1>-Text nach Abschluss der Mutation zu aktualisieren. Das bedeutet, Sie müssen sich nie um das Memoizen von Funktionen oder Objekten kümmern. Tatsächlich müssen Sie sich nicht einmal um Dependency-Listen für Seiteneffekte kümmern, obwohl wir darauf gleich noch eingehen werden.

Aber zuerst sprechen wir über...

State Management

In React verwenden wir den useState Hook, wenn wir Zustände verwalten müssen. Wir übergeben ihm einen Anfangswert, und er gibt ein Tupel mit dem aktuellen Wert und einer Funktion zurück, mit der wir einen neuen Wert setzen können. Es sieht ungefähr so aus

import React, { useState } from "react";

export default function (props) {
  const [number, setNumber] = useState(0);
  return (
    <>
      <h1>Value: {number}</h1>
      <button onClick={() => setNumber(n => n + 1)}>Increment</button>
      <button onClick={() => setNumber(n => n - 1)}>Decrement</button>
    </>
  );
}

Unsere setNumber Funktion kann überallhin übergeben werden, an Kindkomponenten usw.

In Svelte ist es einfacher. Wir können eine Variable erstellen und sie nach Bedarf aktualisieren. Sveltes Ahead-of-Time-Kompilierung (im Gegensatz zu Reacts Just-in-Time-Kompilierung) erledigt die Hintergrundarbeit, um zu verfolgen, wo sie aktualisiert wird, und erzwingt eine Aktualisierung des DOM. Das gleiche einfache Beispiel von oben könnte so aussehen

<script>
  let number = 0;
</script>

<h1>Value: {number}</h1>
<button on:click={() => number++}>Increment</button>
<button on:click={() => number--}>Decrement</button>

Außerdem ist hier zu beachten, dass Svelte kein einzelnes umschließendes Element benötigt, wie es JSX tut. Svelte hat kein Äquivalent zur React-Fragment-Syntax <></>, da diese nicht benötigt wird.

Aber was, wenn wir eine Updater-Funktion an eine Kindkomponente übergeben möchten, damit sie diesen Zustand aktualisieren kann, wie wir es mit React können? Wir können die Updater-Funktion einfach so schreiben

<script>
  import Component3a from "./Component3a.svelte";
        
  let number = 0;
  const setNumber = cb => number = cb(number);
</script>

<h1>Value: {number}</h1>

<button on:click={() => setNumber(val => val + 1)}>Increment</button>
<button on:click={() => setNumber(val => val - 1)}>Decrement</button>

Jetzt übergeben wir sie, wo sie benötigt wird – oder bleiben Sie dran für eine automatisierte Lösung.

Reducer und Stores

React hat auch den useReducer Hook, der es uns ermöglicht, komplexere Zustände zu modellieren. Wir übergeben eine Reducer-Funktion, und sie gibt uns den aktuellen Wert und eine Dispatch-Funktion, mit der wir den Reducer mit einem gegebenen Argument aufrufen können, um dadurch eine Zustandsaktualisierung auszulösen, zu dem, was der Reducer zurückgibt. Unser Zählerbeispiel von oben könnte so aussehen

import React, { useReducer } from "react";

function reducer(currentValue, action) {
  switch (action) {
    case "INC":
      return currentValue + 1;
    case "DEC":
      return currentValue - 1;
  }
}

export default function (props) {
  const [number, dispatch] = useReducer(reducer, 0);
  return (
    <div>
      <h1>Value: {number}</h1>
      <button onClick={() => dispatch("INC")}>Increment</button>
      <button onClick={() => dispatch("DEC")}>Decrement</button>
    </div>
  );
}

Svelte hat *nicht direkt* etwas Vergleichbares, aber was es hat, nennt sich ein **Store**. Die einfachste Art von Store ist ein beschreibbarer (writable) Store. Es ist ein Objekt, das einen Wert enthält. Um einen neuen Wert zu setzen, können Sie set auf dem Store aufrufen und den neuen Wert übergeben, oder Sie können update aufrufen und eine Callback-Funktion übergeben, die den aktuellen Wert erhält und den neuen Wert zurückgibt (genau wie Reacts useState).

Um den aktuellen Wert eines Stores zu einem bestimmten Zeitpunkt zu lesen, gibt es eine get Funktion, die aufgerufen werden kann und ihren aktuellen Wert zurückgibt. Stores haben auch eine subscribe Funktion, der wir einen Callback übergeben können, der jedes Mal ausgeführt wird, wenn sich der Wert ändert.

Da Svelte nun mal Svelte ist, gibt es einige nette syntaktische Abkürzungen für all das. Wenn Sie sich zum Beispiel innerhalb einer Komponente befinden, können Sie einfach ein Store mit einem Dollarzeichen davor prefixen, um seinen Wert zu lesen, oder direkt zuweisen, um seinen Wert zu aktualisieren. Hier ist das Zählerbeispiel von oben, das einen Store verwendet, mit einigen zusätzlichen Seiteneffekt-Logs, um zu demonstrieren, wie subscribe funktioniert

<script>
  import { writable, derived } from "svelte/store";
        
  let writableStore = writable(0);
  let doubleValue = derived(writableStore, $val => $val * 2);
        
  writableStore.subscribe(val => console.log("current value", val));
  doubleValue.subscribe(val => console.log("double value", val))
</script>

<h1>Value: {$writableStore}</h1>

<!-- manually use update -->
<button on:click={() => writableStore.update(val => val + 1)}>Increment</button>
<!-- use the $ shortcut -->
<button on:click={() => $writableStore--}>Decrement</button>

<br />

Double the value is {$doubleValue}

Beachten Sie, dass ich oben auch einen abgeleiteten Store (derived store) hinzugefügt habe. Die Dokumentation behandelt dies ausführlich, aber kurz gesagt: Mit derived Stores können Sie einen Store (oder mehrere Stores) auf einen einzelnen, neuen Wert projizieren, indem Sie die gleiche Semantik wie bei einem beschreibbaren Store verwenden.

Stores in Svelte sind unglaublich flexibel. Wir können sie an Kindkomponenten übergeben, sie ändern, kombinieren oder sie sogar schreibgeschützt machen, indem wir sie über einen abgeleiteten Store weitergeben; wir können sogar einige der React-Abstraktionen neu erstellen, die Ihnen gefallen oder die Sie vielleicht brauchen, wenn wir React-Code nach Svelte konvertieren.

React APIs mit Svelte

Nachdem wir das alles hinter uns gebracht haben, kehren wir zu Reacts useReducer Hook von vorhin zurück.

Nehmen wir an, wir mögen das Definieren von Reducer-Funktionen zur Verwaltung und Aktualisierung von Zuständen sehr. Sehen wir uns an, wie schwierig es wäre, Svelte-Stores zu nutzen, um Reacts useReducer API nachzuahmen. Wir wollen im Grunde unsere eigene useReducer aufrufen, eine Reducer-Funktion mit einem Anfangswert übergeben und einen Store mit dem aktuellen Wert zurückbekommen, sowie eine Dispatch-Funktion, die den Reducer aufruft und unseren Store aktualisiert. Dies zu erreichen ist eigentlich gar nicht so schlecht.

export function useReducer(reducer, initialState) {
  const state = writable(initialState);
  const dispatch = (action) =>
    state.update(currentState => reducer(currentState, action));
  const readableState = derived(state, ($state) => $state);

  return [readableState, dispatch];
}

Die Verwendung in Svelte ist fast identisch mit React. Der einzige Unterschied ist, dass unser aktueller Wert ein Store und kein Rohwert ist, daher müssen wir ihn mit dem $ prefixen, um den Wert zu lesen (oder manuell get oder subscribe darauf aufzurufen).

<script>
  import { useReducer } from "./useReducer";
        
  function reducer(currentValue, action) {
    switch (action) {
      case "INC":
        return currentValue + 1;
      case "DEC":
        return currentValue - 1;
    }
  }
  const [number, dispatch] = useReducer(reducer, 0);      
</script>

<h1>Value: {$number}</h1>

<button on:click={() => dispatch("INC")}>Increment</button>
<button on:click={() => dispatch("DEC")}>Decrement</button>

Was ist mit useState?

Wenn Sie den useState Hook in React wirklich lieben, ist die Implementierung genauso einfach. In der Praxis habe ich diese Abstraktion nicht als nützlich empfunden, aber es ist eine unterhaltsame Übung, die Sveltes Flexibilität wirklich zeigt.

export function useState(initialState) {
  const state = writable(initialState);
  const update = (val) =>
    state.update(currentState =>
      typeof val === "function" ? val(currentState) : val
    );
  const readableState = derived(state, $state => $state);

  return [readableState, update];
}

Sind Two-Way-Bindings *wirklich* böse?

Bevor wir diesen Abschnitt über State Management abschließen, möchte ich noch auf einen letzten Trick eingehen, der spezifisch für Svelte ist. Wir haben gesehen, dass Svelte es uns erlaubt, Updater-Funktionen auf die gleiche Weise wie mit React durch den Komponentenbaum zu leiten. Dies dient oft dazu, Kindkomponenten zu ermöglichen, ihre Eltern über Zustandsänderungen zu informieren. Wir alle haben es schon hundertmal gemacht. Eine Kindkomponente ändert den Zustand irgendwie und ruft dann eine an sie übergebene Funktion von einem Elternteil auf, damit der Elternteil über diese Zustandsänderung informiert wird.

Neben der Unterstützung dieser Übergabe von Callbacks erlaubt Svelte einer Elternkomponente auch, die Zustände eines Kindes zweifach zu binden. Nehmen wir zum Beispiel an, wir haben diese Komponente

<!-- Child.svelte -->
<script>
  export let val = 0;
</script>

<button on:click={() => val++}>
  Increment
</button>

Child: {val}

Dies erstellt eine Komponente mit einer val-Prop. Das Schlüsselwort export ist, wie Komponenten Props in Svelte deklarieren. Normalerweise *übergeben* wir Props an eine Komponente, aber hier machen wir die Dinge ein wenig anders. Wie wir sehen, wird diese Prop von der Kindkomponente modifiziert. In React wäre dieser Code falsch und fehlerhaft, aber mit Svelte kann eine Komponente, die diese Komponente rendert, dies tun

<!-- Parent.svelte -->
<script>
  import Child from "./Child.svelte";
        
  let parentVal;
</script>

<Child bind:val={parentVal} />
Parent Val: {parentVal}

Hier *binden* wir eine Variable in der Elternkomponente an die val-Prop des Kindes. Wenn sich nun die val-Prop des Kindes ändert, wird unsere parentVal von Svelte automatisch aktualisiert.

Two-Way-Binding ist für einige umstritten. Wenn Sie das hassen, können Sie es ruhig nie benutzen. Aber sparsam eingesetzt, habe ich es als ein unglaublich nützliches Werkzeug zur Reduzierung von Boilerplate-Code empfunden.

Seiteneffekte in Svelte, ohne Tränen (oder Stale Closures)

In React verwalten wir Seiteneffekte mit dem useEffect Hook. Es sieht so aus

useEffect(() => {
  console.log("Current value of number", number);
}, [number]);

Wir schreiben unsere Funktion mit der Dependency-Liste am Ende. Bei jedem Render inspiziert React jeden Eintrag in der Liste, und wenn einer davon referenziell anders ist als beim letzten Render, wird der Callback erneut ausgeführt. Wenn wir nach der letzten Ausführung aufräumen möchten, können wir eine Cleanup-Funktion aus dem Effekt zurückgeben.

Für einfache Dinge, wie eine sich ändernde Zahl, ist es einfach. Aber wie jeder erfahrene React-Entwickler weiß, kann useEffect für nicht-triviale Anwendungsfälle tückisch schwierig sein. Es ist überraschend einfach, versehentlich etwas aus dem Dependency-Array wegzulassen und eine Stale Closure zu erhalten.

In Svelte ist die grundlegendste Form der Behandlung eines Seiteneffekts eine reaktive Anweisung, die so aussieht

$: {
  console.log("number changed", number);
}

Wir prefixen einen Codeblock mit $: und platzieren den Code, den wir ausführen möchten, darin. Svelte analysiert, welche Abhängigkeiten gelesen werden, und wann immer sie sich ändern, führt Svelte unseren Block erneut aus. Es gibt keine direkte Möglichkeit, das Aufräumen vom letzten Mal, als der reaktive Block ausgeführt wurde, auszuführen, aber es ist einfach genug, dies zu umgehen, wenn wir es wirklich brauchen

let cleanup;
$: {
  cleanup?.();
  console.log("number changed", number);
  cleanup = () => console.log("cleanup from number change");
}

Nein, dies führt nicht zu einer Endlosschleife: Neuzuweisungen innerhalb eines reaktiven Blocks lösen den Block nicht erneut aus.

Während dies funktioniert, müssen diese Cleanup-Effekte normalerweise ausgeführt werden, wenn Ihre Komponente unmounted wird, und Svelte hat dafür eine Funktion eingebaut: Es gibt eine onMount Funktion, die es uns erlaubt, eine Cleanup-Funktion zurückzugeben, die ausgeführt wird, wenn die Komponente zerstört wird, und noch direkter, es gibt auch eine onDestroy Funktion, die das tut, was man erwartet.

Mit Actions aufpeppen

Das Vorherige funktioniert alles gut genug, aber Svelte glänzt wirklich mit Actions. Seiteneffekte sind häufig an unsere DOM-Knoten gebunden. Wir möchten vielleicht ein altes (aber immer noch großartiges) jQuery-Plugin an einem DOM-Knoten integrieren und es abreißen, wenn dieser Knoten die DOM verlässt. Oder vielleicht möchten wir einen ResizeObserver für einen Knoten einrichten und ihn abreißen, wenn der Knoten die DOM verlässt, und so weiter. Dies ist eine häufige Anforderung, die Svelte mit Actions integriert. Sehen wir uns an, wie.

{#if show}
  <div use:myAction>
    Hello                
  </div>
{/if}

Beachten Sie die use:actionName-Syntax. Hier haben wir dieses <div> mit einer Aktion namens myAction verknüpft, die nur eine Funktion ist.

function myAction(node) {
  console.log("Node added", node);
}

Diese Aktion wird ausgeführt, wenn das <div> in die DOM eintritt, und übergibt den DOM-Knoten daran. Dies ist unsere Chance, unsere jQuery-Plugins hinzuzufügen, unseren ResizeObserver einzurichten usw. Nicht nur das, sondern wir können auch eine Cleanup-Funktion daraus zurückgeben, so

function myAction(node) {
  console.log("Node added", node);

  return {
    destroy() {
      console.log("Destroyed");
    }
  };
}

Nun wird der destroy()-Callback ausgeführt, wenn der Knoten die DOM verlässt. Hier reißen wir unsere jQuery-Plugins usw. ab.

Aber warten Sie, da ist mehr!

Wir können sogar Argumente an eine Aktion übergeben, so

<div use:myAction={number}>
  Hello                
</div>

Dieses Argument wird als zweites Argument an unsere Aktionsfunktion übergeben

function myAction(node, param) {
  console.log("Node added", node, param);

  return {
    destroy() {
      console.log("Destroyed");
    }
  };
}

Und wenn Sie zusätzliche Arbeiten durchführen möchten, wann immer sich dieses Argument ändert, können Sie eine Update-Funktion zurückgeben

function myAction(node, param) {
  console.log("Node added", node, param);

  return {
    update(param) {
      console.log("Update", param);
    },
    destroy() {
      console.log("Destroyed");
    }
  };
}

Wenn sich das Argument unserer Aktion ändert, wird die Update-Funktion ausgeführt. Um mehrere Argumente an eine Aktion zu übergeben, übergeben wir ein Objekt

<div use:myAction={{number, otherValue}}>
  Hello                
</div>

…und Svelte führt unsere Update-Funktion jedes Mal erneut aus, wenn sich eine der Eigenschaften des Objekts ändert.

Actions sind eine meiner Lieblingsfunktionen von Svelte; sie sind unglaublich leistungsfähig.

Kleinkram

Svelte liefert auch eine Reihe von großartigen Funktionen, die keine Entsprechung in React haben. Es gibt eine Reihe von Formular-Bindings (die das Tutorial behandelt) sowie CSS Helfer.

Entwickler, die von React kommen, werden vielleicht überrascht sein zu erfahren, dass Svelte auch Animationsunterstützung out-of-the-box mitliefert. Anstatt auf npm zu suchen und auf das Beste zu hoffen, ist es... eingebaut. Es enthält sogar Unterstützung für Federphysik sowie Ein- und Ausblendungsanimationen, die Svelte **Transitions** nennt.

Sveltes Antwort auf React.Chidren sind Slots, die benannt oder unbenannt sein können und in der Svelte-Dokumentation gut behandelt werden. Ich habe sie als einfacher zu verstehen empfunden als Reacts Children API.

Zuletzt, eine meiner Lieblings-„fast versteckten“ Funktionen von Svelte ist, dass es seine Komponenten in tatsächliche Web Components kompilieren kann. Der svelte:options-Helfer hat eine tagName-Eigenschaft, die dies ermöglicht. Aber stellen Sie sicher, dass Sie die entsprechende Eigenschaft in der webpack- oder Rollup-Konfiguration einstellen. Mit webpack sähe das ungefähr so aus

{
  loader: "svelte-loader",
  options: {
    customElement: true
  }
}

Interesse, Svelte auszuprobieren?

Jeder dieser Punkte wäre für sich allein ein großartiger Blogbeitrag. Obwohl wir bei Themen wie State Management und Actions nur an der Oberfläche gekratzt haben, haben wir gesehen, wie Sveltes Funktionen nicht nur mit React mithalten können, sondern sogar viele React-APIs nachahmen können. Und das, bevor wir kurz auf Sveltes Annehmlichkeiten eingegangen sind, wie eingebaute Animationen (oder Transitions) und die Möglichkeit, Svelte-Komponenten in echte Web Components umzuwandeln.

Ich hoffe, ich konnte ein gewisses Interesse wecken, und wenn ja, gibt es keinen Mangel an Dokumentationen, Tutorials, Online-Kursen usw., die sich mit diesen Themen (und mehr) befassen. Lassen Sie mich in den Kommentaren wissen, wenn Sie Fragen haben!