App von React zu Svelte umschreiben, was Sie erwarten können

Published: 2021-04-08

Kürzlich habe ich eine mittelgroße App in React (~250 React-Komponenten) in Svelte umgeschrieben. Das Ergebnis dieser Aktion ist eine App, die effizienter ist, einfacher zu warten und einen viel besser lesbaren Code hat. Aber es gab einige Probleme, einige Situationen, die nicht einfach in Svelte-Code zu transformieren sind. Dieser Artikel beschreibt einige dieser Situationen.

Dies ist kein Svelte-Tutorial für React-Programmierer. Wenn Sie Svelte noch nicht kennen, ist der beste Ausgangspunkt das offizielle Svelte-Tutorial. Dieser Artikel enthält viele Links zu Svelte-Tutorials, anstatt Svelte-Prinzipien zu erklären.

CSS

Svelte unterstützt CSS direkt auf eine sehr schöne und saubere Weise. In der React-App habe ich styled components verwendet. Es gibt viele CSS-Bibliotheken für React, aber alle (soweit ich weiß) verwenden das CSS-in-JS-Paradigma, während Sie in Svelte ziemlich sauberes CSS verwenden.

Im folgenden Snippet zeige ich eine vereinfachte Implementierung einer Toolbar-Schaltfläche, in React (mit styled components) und mit Svelte.

Die Toolbar-Schaltfläche hat eine Eigenschaft (disabled), die die Hover-Funktionalität deaktiviert. Außerdem verwendet sie einige Variablen aus dem Theme.

// React-Komponente ToolbarButton({disabled, toolBarHeight})

const ToolbarButton = styled.div`
  padding: 5px 15px;
  height: ${props.toolBarHeight}px;

  ${(props) =>
    !props.disabled &&
    `
  &:hover {
    background-color: gray;
  }
  `}
`;
<!-- Svelte-Komponente -->

<script>
  export let disabled;
</script>

<div class="button" class:disabled />

<style>
.button {
  padding: 5px 15px;
  height: var(--theme-toolbar-height); /* Sie können die Eigenschaft nicht direkt verwenden, es muss eine CSS-Variable verwendet werden */
}
.button.disabled {
  background-color: gray;
}
</style>

Svelte unterstützt keine Variablen in CSS. Es ist also nicht möglich, eine Eigenschaft an eine Svelte-Komponente zu übergeben und sie in CSS zu verwenden. Der einzige Weg, dies zu tun, ist das Ein- und Ausschalten der CSS-Klasse. Der Selektor button.disabled wird verwendet, wenn disabled auf true gesetzt ist.

Wenn Sie einige Werte (z.B. Dimensionen in CSS) verwenden möchten, können Sie CSS-Variablen verwenden.

React-Hooks, insbesondere useEffect

React-Hooks sind eine sehr leistungsstarke Funktion. In Svelte werden Sie nichts Ähnliches finden. Aber die meisten React-Hooks werden Sie in Svelte nicht vermissen.

  • useState - Zustand wird mit let-Befehlen definiert
  • useMemo - Sie können reaktive Befehle verwenden, um den internen Zustand neu zu berechnen
  • useCallback - Funktionsausdrücke werden nur einmal ausgewertet, daher macht es keinen Sinn
  • useContext - Svelte hat eine sehr einfache und unkomplizierte Kontext-API (getContext(), setContext())
  • useReducer - Sie müssen eine Svelte-Version erstellen, die Stores verwendet (das könnte sehr einfach sein)
  • useRef - Sie können stattdessen bind:this verwenden (oder eine let-Variable für Instanzvariablen)
  • useEffect - das ist komplizierter…

useEffect - einfache Verwendung

Ich verwende useEffect in React sehr oft, leider müssen Sie darüber nachdenken, wie Sie die gleiche Logik in Svelte implementieren.

Die einfachste Verwendung ist das Ausführen von Code beim Mounten und Unmounten.

// React
React.useEffect(() => {
  console.log('MOUNT');
  return () => console.log('UNMOUNT');
}, []);

Diese Variante wird mit den Svelte-Methoden onMount und onDestroy abgedeckt

// Svelte

onMount(() => {
  console.log('MOUNT');
  return () => console.log('UNMOUNT 1');
});
onDestroy(() => {
  console.log('UNMOUNT 2');
});

Wie Sie sehen können, haben Sie zwei Varianten, wie Sie Code beim Unmounten ausführen können - mit der onDestroy-Funktion und mit dem Rückgabewert der onMount-Funktion.

useEffect - reaktive Anweisung

Wenn Sie Code bei einer Ausdrucksänderung ausführen möchten, können Sie die Svelte reaktive Anweisung verwenden.

// React
React.useEffect(() => {
  console.log('HEIGHT changed, new value:', height);
}, [height]);
// Svelte
$: console.log('HEIGHT changed, new value:', height); 

In Svelte wird die reaktive Anweisung ausgeführt, wenn eine der Abhängigkeiten der Anweisung zugewiesen wurde. Wenn Sie also etwas nur tun müssen, wenn sich die Abhängigkeit ändert, müssen Sie den Wert selbst überprüfen.

Wenn Sie die Abhängigkeiten explizit aufzählen möchten (wie im zweiten Argument der React.useEffect-Methode), können Sie dieses Muster verwenden:

// Svelte

$: {
  height;
  width;
  handleChanged(); 
}

Dies wird nach der Zuweisung der height- oder width-Variable aufgerufen. Sie können andere Variablen in der handleChanged-Funktion verwenden, aber diese Abhängigkeiten werden diese reaktive Anweisung nicht auslösen.

useEffect - reaktive Anweisung mit Bereinigung

React useEffect kann auch verwendet werden, um einige Ressourcen zuzuweisen, die von einem Eigenschaftswert abhängen. Im folgenden Beispiel zeigt die Komponente eine Liste von Tabellen in einer SQL-Datenbank an. Sie hört auf Änderungen der Datenbankstruktur. Wenn sich die Eigenschaft connectionId ändert, ist es notwendig, die Verbindung zum alten Socket zu trennen und eine Verbindung zum neuen Socket herzustellen.

// React
function SqlTableList({ connectionId }) {
  React.useEffect(() => {
    socket.on(`database-structure-changed-${connectionId}`, handleDatabaseStructureChanged);
    return () => {
      socket.off(`database-structure-changed-${connectionId}`, handleDatabaseStructureChanged);
    };
  }, [connectionId]);
}

Svelte unterstützt dieses Szenario nicht so direkt wie React. Aber Sie können einen einfachen Trick verwenden, um dieses Verhalten zu erreichen.

// Svelte

const useEffect = subscribe => ({ subscribe });

$: effect = useEffect(() => {
  socket.on(`database-structure-changed-${connectionId}`, handleDatabaseStructureChanged);
  return () => {
    socket.off(`database-structure-changed-${connectionId}`, handleDatabaseStructureChanged);
  };
});

$: $effect;

Wie funktioniert dieser Trick? Sie müssen über Stores Bescheid wissen, insbesondere über benutzerdefinierte Stores (Tutorial). Die Funktion useEffects erstellt benutzerdefinierte Stores mit einer Abonnementmethode. Die subscribe-Methode des Stores muss eine unsubscribe-Methode zurückgeben, die aufgerufen wird, wenn das Store-Abonnement nicht mehr benötigt wird.

Die letzte Zeile ( $: $effect ) verwaltet das automatische Store-Abonnement (Tutorial). So wird korrekt socket.on aufgerufen und dann socket.off, wenn sich der Wert von connectionId ändert (oder nur in Svelte zugewiesen wird), ähnlich wie es das React-Gegenstück tut.

Warum ich React-Props in Svelte vermisse

Natürlich gibt es auch in Svelte Props. Sie funktionieren ähnlich wie in React. Aber in React sind die einzigen externen Schnittstellen der Komponente die Props. In Svelte haben Sie mehrere Mechanismen, um das Verhalten Ihrer Komponenten zu verwalten:

  • props - funktioniert wie in React
  • events - verwenden Sie eine spezielle Syntax, Ereignisse sind nicht Teil des $$props-Objekts, das alle Props enthält
  • actions (Syntax use:action) - Mechanismus, um Logik zu wiederverwenden, die an HTML-Elemente gebunden ist
  • slots - Zweck ist derselbe wie die React-Eigenschaft children, mit einigen Erweiterungen

All diese Mechanismen sind sehr nützlich, aber sie haben keinen einheitlichen Zugriff wie Props in React. Unten werden einige Probleme diskutiert, auf die ich gestoßen bin.

Weiterleiten von Ereignissen

Wenn Sie komplexere Komponentenhierarchien erstellen, leiten einige Komponenten nur Daten von übergeordneten Komponenten weiter.

// React - leitet alle Ereignisse weiter, sie sind Teil der Props

function Outer(props) {
  return <Inner {...props} />;
}
 <!-- Svelte - nur explizit benannte Ereignisse click und keydown werden weitergeleitet -->

 <Inner {...$$props} on:click on:keydown />;
}

In Svelte können Sie nicht alle Ereignisse weiterleiten, aber Sie können aufgezählte Ereignisse weiterleiten. Wenn Sie jedoch alle Ereignisse weiterleiten müssen, können Sie Callback-Funktionen verwenden (onClick anstelle von on:click), dann wird onClick Teil von $$props.

Implementierung von TabControl

In React ist es ziemlich einfach, eine TabControl-Komponente zu implementieren, die wie folgt verwendet wird:

// React

<TabControl>
  <TabPage label='Seite 1'>
    Inhalt der Seite 1
  </TabPage>
  <TabPage label='Seite 2'>
    Inhalt der Seite 2
  </TabPage>
</TabControl>

Die Implementierung wird durch das children-Array durchlaufen und kann leicht die gewünschten Informationen (Seitentitel und Inhalt) extrahieren. Dieser Ansatz funktioniert in Svelte nicht, es gibt nichts Vergleichbares wie children. Sie können Svelte-Fragmente verwenden, die Verwendung sieht folgendermaßen aus:

 <!-- Svelte -->

<TabControl tabs={[
  { label: 'Seite 1', slot: 1},
  { label: 'Seite 2', slot: 2},
  ]}>
  <svelte:fragment slot='1'>
    Inhalt der Seite 1
  </svelte:fragment>
  <svelte:fragment slot='2'>
    Inhalt der Seite 2
  </svelte:fragment>
</TabControl>

Tabs werden in einem Array definiert, das Tab-Layout (children) wird in Fragmenten definiert, die als Slots an die TabControl-Komponente übergeben werden. Es ist nicht so intuitiv wie in React, aber es funktioniert. Nur ein ziemlich großes Problem, das Sie in der TabControl-Implementierung sehen können:

// TabControl.svelte

<script>
  export let tabs = []
</script>

<div>
 {#each _.compact(tabs) as tab, index}
   <div class="container" class:isInline class:tabVisible={index == value}>
     {#if tab.slot == 0}<slot name="0" />
     {:else if tab.slot == 1}<slot name="1" />
     {:else if tab.slot == 2}<slot name="2" />
     {:else if tab.slot == 3}<slot name="3" />
     {:else if tab.slot == 4}<slot name="4" />
     {:else if tab.slot == 5}<slot name="5" />
     {:else if tab.slot == 6}<slot name="6" />
     {:else if tab.slot == 7}<slot name="7" />
     {/if}
   </div>
 {/each}
</div>

Slot-Namen müssen statische Zeichenfolgen sein, daher müssen Sie so etwas tun, damit es funktioniert.

Verwendung einer dynamischen Komponente

Ein anderer Ansatz könnte darin bestehen, eine Komponente pro Tab-Seite zu definieren, aber dann haben Sie viele kleine Dateien, da jede Svelte-Komponente in ihrer eigenen Datei definiert werden muss.

 <!-- Svelte -->

<TabControl tabs={[
  { label: 'Seite 1', component: Tab1},
  { label: 'Seite 2', component: Tab2},
  ]}/>

// Tab1.svelte
Inhalt der Seite 1
// Tab2.svelte
Inhalt der Seite 2

Die Implementierung wird svelte:component verwenden, um die richtige Registerkarte zu instanziieren.

Verwendung von Kontext

Dies ist der am wenigsten offensichtliche Weg. Aber tatsächlich können Sie die gleiche Syntax wie in React erreichen.

 <!-- Svelte -->

<TabControl>
  <TabPage label='Seite 1'>
    Inhalt der Seite 1
  </TabPage>
  <TabPage label='Seite 2'>
    Inhalt der Seite 2
  </TabPage>
</TabControl>

In der TabControl-Komponente muss ein “Sammelpunkt” definiert werden, z.B. ein Array von Kind-Tabs

<!-- TabControl.svelte -->
<script>
  const tabs = [];
  setContext('tabs', tabs);
</script>

Und in der Tab-Seite registrieren wir nur die Registerkarte im übergeordneten Array:

<!-- TabPage.svelte -->
<script>
  export let label;
  const tabs = getContext('tabs');
  tabs.push({ label });
</script>

Das einzige Problem bei dieser Methode ist, dass sie die definierte Reihenfolge der Tab-Seiten nicht beibehält, wenn einige von ihnen bedingt gerendert werden.


<TabControl>
  {#if condition_will_be_true_later}
    <TabPage label='Seite 1'>
      Inhalt der Seite 1
    </TabPage>
  {/if}
  <TabPage label='Seite 2'>
    Inhalt der Seite 2
  </TabPage>
</TabControl>

Dies wird mit zuerst Seite 2 und dann Seite 1 gerendert, was wahrscheinlich nicht das gewünschte Ergebnis ist.

Vorsicht bei der Verwendung von bind:clientHeight und bind:clientWidth

Das Binden von Dimensionen (Tutorial) ist eine großartige Funktion. Aber seien Sie vorsichtig, wenn Sie es verwenden. Es wird mit einem versteckten iframe implementiert (Sie können es in diesem REPL sehen). Aus meinen Erfahrungen heraus hat es in komplizierteren Situationen manchmal in FireFox nicht funktioniert. Manchmal ist es sicherer, ResizeObserver zu verwenden (siehe diese Implementierung einer Aktion).

Fehlergrenze

React hat ein großartiges Konzept von Fehlergrenzen (“try-catch” für Komponenten). Es wird in funktionalen Komponenten nicht unterstützt (tatsächlich war ErrorBoundary die einzige Klassenkomponente in meiner React-Anwendung), aber es war kein Problem, diese Klassenkomponente in einer funktionalen React-Anwendung zu verwenden.

// React
<ErrorBoundary>
  {(null).read()}
</ErrorBoundary>

Ohne Fehlergrenze, wenn Sie diesen Code in einer React-Anwendung haben, wird die gesamte Anwendung fehlschlagen. Mit einer Fehlergrenze wird nur das Innere der Grenze fehlschlagen, andere Teile der Anwendung werden wie gewohnt gerendert.

Leider hat Svelte nichts Vergleichbares. Es gibt ein NPM-Paket namens svelte-error-boundary, aber tatsächlich löst es nur einen kleinen Teil der Probleme und die Mehrheit der Fehler in der Svelte-App führt immer noch zu einem Absturz der App.

Svelte hat seine eigene Art des Absturzes - Svelte-Komponenten hören auf, reaktiv zu sein, sodass die App eingefroren aussieht.

Das Einzige, was Sie tun können, ist, diese Situation zu erkennen und dem Benutzer zu erlauben, die Seite neu zu laden.

<!-- Svelte -->

<script>
  let counter = 0;
  $: counterCopy = counter;
  const onunhandledrejection = async e => {
    console.log('Unbehandelter Fehler, Überprüfung, ob abgestürzt', e);
    const oldCounter = counter;
    counter++;
    window.setTimeout(() => {
      if (counterCopy <= oldCounter) {
        console.log('ABSTURZ ERKANNT!!!');
        if (window.confirm('Entschuldigung, die App ist abgestürzt.\nSeite neu laden?')) {
          window.location.reload();
        }
      }
    }, 500);
  };
</script>

<svelte:window on:unhandledrejection={onunhandledrejection} />

Zum Erkennen wird das unhandledrejection-Ereignis verwendet. Dieses Ereignis kann in mehreren Situationen ausgelöst werden, einige davon führen nicht zu einem Svelte-Absturz. Dies ist der Grund für die Verwendung der Variablen counter und counterCopy. Wenn die reaktive Anweisung nicht funktioniert, bedeutet dies, dass das gesamte Svelte abgestürzt ist und der einzige Weg zur Wiederherstellung das Neuladen der Seite ist.

Sie können die vollständige ErrorHandler-Komponente sehen.

Fazit

Trotz dieser Probleme ist Svelte ein großartiges Framework und ich bin sehr zufrieden mit dem Ergebnis der Umstellung meiner App von React auf Svelte. Einige Funktionen, die jetzt mit Svelte implementiert sind, wären mit React nahezu unmöglich zu realisieren.

Natürlich, wenn Sie viele Drittanbieter-Bibliotheken verwenden, die nur für React verfügbar sind, könnte das ein ernstes Problem sein. Aber das war nicht mein Fall, ich hatte minimale Abhängigkeiten mit React, in diesem Fall könnte auch das Umschreiben zu Svelte ziemlich einfach und schnell sein.