App von React auf Svelte umschreiben – was Sie erwarten können
Published: 2021-04-08 | Jan Procházka
Kürzlich habe ich eine mittelgroße App in React (~250 React-Komponenten) nach Svelte umgeschrieben. Das Ergebnis ist eine App, die effizienter ist, sich leichter warten lässt und einen deutlich besser lesbaren Code hat. Es gab aber auch einige Probleme, Situationen, die sich nicht ganz so einfach in Svelte-Code übertragen lassen. Dieser Artikel beschreibt einige dieser Situationen.
Dies ist kein Svelte-Tutorial für React-Programmierer. Wenn Sie Svelte noch nicht kennen, ist der beste Einstieg das offizielle Svelte-Tutorial. Dieser Artikel enthält viele Links zu Svelte-Tutorials, anstatt Svelte-Grundlagen im Detail 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ß) nutzen das CSS-in-JS‑Paradigma, während Sie in Svelte recht „reines“ CSS verwenden.
Im folgenden Snippet zeige ich eine vereinfachte Implementierung eines Toolbar-Buttons – einmal in React (mit styled components) und einmal in Svelte.
Der Toolbar-Button hat eine Eigenschaft (disabled), die die Hover-Funktionalität deaktiviert. Außerdem nutzt er einige Variablen aus dem Theme.
// React component ToolbarButton({disabled, toolBarHeight})
const ToolbarButton = styled.div`
padding: 5px 15px;
height: ${props.toolBarHeight}px;
${(props) =>
!props.disabled &&
`
&:hover {
background-color: gray;
}
`}
`;
<!-- Svelte component -->
<script>
export let disabled;
</script>
<div class="button" class:disabled />
<style>
.button {
padding: 5px 15px;
height: var(--theme-toolbar-height); /* you cannot use prop directly, CSS variable must be used */
}
.button.disabled {
background-color: gray;
}
</style>
Svelte unterstützt keine Variablen direkt in CSS. Es ist also nicht möglich, eine Property an eine Svelte-Komponente zu übergeben und sie direkt im CSS zu verwenden. Die einzige Möglichkeit ist, CSS-Klassen ein- oder auszuschalten. Der Selektor .button.disabled wird verwendet, wenn disabled auf true gesetzt ist.
Wenn Sie bestimmte Werte (z. B. Dimensionen im CSS) verwenden möchten, können Sie CSS-Variablen nutzen.
React Hooks, insbesondere useEffect
React Hooks sind eine sehr mächtige Funktion. In Svelte finden Sie nichts direkt Vergleichbares. Die meisten React Hooks werden Sie in Svelte aber nicht vermissen.
- useState – State wird mit
let-Anweisungen definiert - useMemo – Sie können stattdessen reaktive Anweisungen verwenden, um internen State neu zu berechnen
- useCallback – Funktionsausdrücke werden nur einmal ausgewertet, daher ergibt dieser Hook wenig Sinn
- useContext – Svelte hat eine sehr einfache und direkte Context-API (
getContext(),setContext()) - useReducer – Sie müssen eine Svelte-Version mit Stores erstellen (das kann sehr einfach sein)
- useRef – Sie können stattdessen
bind:thisverwenden (oder einelet-Variable für Instanzvariablen) - useEffect – das ist etwas komplizierter …
useEffect – einfache Verwendung
Ich nutze useEffect in React sehr häufig; leider muss man sich in Svelte genau überlegen, wie man dieselbe Logik umsetzt.
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 in Svelte mit den Methoden onMount und onDestroy abgedeckt.
// Svelte
onMount(() => {
console.log('MOUNT');
return () => console.log('UNMOUNT 1');
});
onDestroy(() => {
console.log('UNMOUNT 2');
});
Wie Sie sehen, haben Sie zwei Varianten, um Code beim Unmount auszuführen – mit der Funktion onDestroy und mit dem Rückgabewert von onMount.
useEffect – reaktive Anweisung
Wenn Sie Code bei Änderung eines Ausdrucks ausführen möchten, können Sie Sveltes reaktive Anweisungen verwenden.
// React
React.useEffect(() => {
console.log('HEIGHT changed, new value:', height);
}, [height]);
// Svelte
$: console.log('HEIGHT changed, new value:', height);
In Svelte wird eine reaktive Anweisung ausgeführt, wenn eine ihrer Abhängigkeiten neu zugewiesen wurde. Wenn Sie also etwas nur dann tun möchten, wenn sich eine Abhängigkeit tatsächlich ändert, müssen Sie den Wert selbst prüfen.
Wenn Sie Abhängigkeiten explizit aufzählen möchten (wie im zweiten Argument von React.useEffect), können Sie folgendes Muster verwenden:
// Svelte
$: {
height;
width;
handleChanged();
}
Dies wird nach einer Zuweisung an height oder width aufgerufen. Sie können in handleChanged weitere Variablen verwenden, aber diese zusätzlichen Abhängigkeiten lösen diese reaktive Anweisung nicht aus.
useEffect – reaktive Anweisung mit Cleanup
React useEffect kann auch verwendet werden, um Ressourcen zu allokieren, die von einem Property-Wert abhängen. Im folgenden Beispiel zeigt die Komponente eine Liste von Tabellen in einer SQL-Datenbank an. Sie lauscht auf Änderungen an der Datenbankstruktur. Wenn sich die Property connectionId ändert, muss die Komponente die alte Socket-Verbindung trennen und eine neue aufbauen.
// 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. Sie können aber 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 Stores kennen, insbesondere Custom Stores (Tutorial).
Die Funktion useEffect erzeugt einen Custom Store mit einer subscribe-Methode. Die subscribe-Methode eines Stores muss eine Unsubscribe-Funktion zurückgeben, die aufgerufen wird, wenn das Store-Subscription nicht mehr benötigt wird.
Die letzte Zeile ($: $effect) sorgt für die automatische Store-Subscription (Tutorial). Dadurch werden socket.on und anschließend socket.off korrekt aufgerufen, wenn sich der Wert von connectionId ändert (oder in Svelte neu zugewiesen wird) – ähnlich wie beim React-Pendant.
Warum ich React-Props in Svelte vermisse
Natürlich gibt es auch in Svelte Props. Sie funktionieren ähnlich wie in React. In React sind Props aber die einzige externe Schnittstelle einer Komponente. In Svelte haben Sie mehrere Mechanismen, um das Verhalten Ihrer Komponenten zu steuern:
- Props – funktionieren wie in React
- Events – spezielle Syntax; Events sind kein Teil des
$$props-Objekts, das alle Props enthält - Actions (Syntax
use:action) – Mechanismus, um Logik wiederzuverwenden, die an HTML-Elemente gebunden ist - Slots – Zweck ist derselbe wie bei der React-Property
children, mit einigen Erweiterungen
Alle diese Mechanismen sind sehr nützlich, aber sie haben keinen einheitlichen Zugriff wie Props in React. Im Folgenden werden einige Probleme beschrieben, auf die ich gestoßen bin.
Events weiterleiten
Wenn Sie komplexere Komponenten-Hierarchien erstellen, leiten einige Komponenten nur Daten von Elternkomponenten weiter.
// React - forwards all events, they are part of props
function Outer(props) {
return <Inner {...props} />;
}
<!-- Svelte - only explicitly named events click and keydown are forwarded -->
<Inner {...$$props} on:click on:keydown />;
}
In Svelte können Sie nicht alle Events weiterleiten, sondern nur explizit aufgezählte Events. Wenn Sie wirklich alle Events weiterleiten müssen, können Sie Callback-Funktionen verwenden (onClick statt on:click); dann ist onClick Teil von $$props.
Implementierung eines TabControl
In React ist es recht einfach, eine TabControl-Komponente zu implementieren, die wie folgt verwendet wird:
// React
<TabControl>
<TabPage label='Page 1'>
Page 1 content
</TabPage>
<TabPage label='Page 2'>
Page 2 content
</TabPage>
</TabControl>
Die Implementierung iteriert über das children-Array und kann so leicht die gewünschten Informationen (Seitentitel und Inhalt) extrahieren. Dieser Ansatz funktioniert in Svelte nicht, da es kein children gibt. Sie können Svelte-Fragmente verwenden; die Verwendung sieht dann so aus:
<!-- Svelte -->
<TabControl tabs={[
{ label: 'Page 1', slot: 1},
{ label: 'Page 2', slot: 2},
]}>
<svelte:fragment slot='1'>
Page 1 content
</svelte:fragment>
<svelte:fragment slot='2'>
Page 2 content
</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. Das ist nicht ganz so intuitiv wie in React, funktioniert aber. Es gibt nur eine ziemlich große Stolperfalle, die 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 Strings sein, daher müssen Sie so etwas tun, damit es funktioniert.
Verwendung einer dynamischen Komponente
Ein anderer Ansatz wäre, eine Komponente pro Tab-Seite zu definieren, aber dann hätten Sie viele kleine Dateien, da jede Svelte-Komponente in einer eigenen Datei definiert sein muss.
<!-- Svelte -->
<TabControl tabs={[
{ label: 'Page 1', component: Tab1},
{ label: 'Page 2', component: Tab2},
]}/>
// Tab1.svelte
Page 1 content
// Tab2.svelte
Page 2 content
Die Implementierung verwendet svelte:component, um den passenden Tab zu instanziieren.
Verwendung von Context
Das ist der am wenigsten offensichtliche Weg. Tatsächlich können Sie damit aber dieselbe Syntax wie in React erreichen.
<!-- Svelte -->
<TabControl>
<TabPage label='Page 1'>
Page 1 content
</TabPage>
<TabPage label='Page 2'>
Page 2 content
</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 den Tab nur im Eltern-Array:
<!-- TabPage.svelte -->
<script>
export let label;
const tabs = getContext('tabs');
tabs.push({ label });
</script>
Die einzige Stolperfalle bei dieser Methode ist, dass sie die definierte Reihenfolge der Tab-Seiten nicht beibehält, wenn einige davon bedingt gerendert werden.
<TabControl>
{#if condition_will_be_true_later}
<TabPage label='Page 1'>
Page 1 content
</TabPage>
{/if}
<TabPage label='Page 2'>
Page 2 content
</TabPage>
</TabControl>
Dies wird so gerendert, dass zuerst „Page 2“ und dann „Page 1“ erscheint – was vermutlich 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. Seien Sie aber vorsichtig bei der Verwendung. Es ist mit einem versteckten iframe implementiert (Sie können ihn in diesem REPL sehen). Nach meiner Erfahrung funktioniert es in komplexeren Situationen manchmal in Firefox nicht richtig. Manchmal ist es sicherer, ResizeObserver zu verwenden (siehe diese Implementierung einer Action).
Error Boundary
React hat ein großartiges Konzept von Error Boundaries („try-catch“ für Komponenten). Es wird in Funktionskomponenten 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-App zu verwenden.
// React
<ErrorBoundary>
{(null).read()}
</ErrorBoundary>
Ohne Error Boundary würde bei solchem Code in einer React-Anwendung die gesamte App abstürzen. Mit Error Boundary schlägt nur der innere Teil fehl, andere Teile der Anwendung werden wie gewohnt gerendert.
Leider hat Svelte nichts Vergleichbares. Es gibt ein NPM-Paket namens svelte-error-boundary, das aber in Wirklichkeit nur einen kleinen Teil der Probleme löst; die Mehrheit der Fehler in einer Svelte-App führt weiterhin zum Absturz der App.
Svelte hat seine eigene Art des Absturzes – Svelte-Komponenten hören auf, reaktiv zu sein, sodass die App wie eingefroren wirkt.
Das Einzige, was Sie tun können, ist, diese Situation zu erkennen und den Benutzer die Seite neu laden zu lassen.
<!-- Svelte -->
<script>
let counter = 0;
$: counterCopy = counter;
const onunhandledrejection = async e => {
console.log('Unhandler error, checking whether crashed', e);
const oldCounter = counter;
counter++;
window.setTimeout(() => {
if (counterCopy <= oldCounter) {
console.log('CRASH DETECTED!!!');
if (window.confirm('Sorry, App has crashed.\nReload page?')) {
window.location.reload();
}
}
}, 500);
};
</script>
<svelte:window on:unhandledrejection={onunhandledrejection} />
Zur Erkennung wird das unhandledrejection-Event verwendet. Dieses Event kann in mehreren Situationen ausgelöst werden, von denen einige keinen Svelte-Absturz verursachen. Das ist der Grund für die Verwendung der Variablen counter und counterCopy. Wenn die reaktive Anweisung nicht mehr funktioniert, bedeutet das, dass Svelte insgesamt abgestürzt ist und die einzige Möglichkeit zur Wiederherstellung ein Neuladen der Seite ist.
Die vollständige Komponente finden Sie unter ErrorHandler.
Fazit
Trotz dieser Probleme ist Svelte ein großartiges Framework, und ich bin mit dem Ergebnis der Umstellung meiner App von React auf Svelte sehr zufrieden. Einige Features, die jetzt mit Svelte implementiert sind, wären mit React nahezu unmöglich umzusetzen.
Natürlich kann es ein ernstes Problem sein, wenn Sie viele Drittanbieter-Bibliotheken verwenden, die nur für React verfügbar sind. Das war bei mir jedoch nicht der Fall; meine Abhängigkeiten von React waren minimal. In so einem Fall kann das Umschreiben nach Svelte recht einfach und schnell sein.