Die Reaktive Offenbarung:
Eine Einführung in Svelte

Nils Röhrig | Loql

Agenda

  1. Was ist Svelte?
  2. Die Anatomie einer Svelte-Komponente
  3. Komposition von Komponenten in Svelte
  4. Reaktive Goodies in Svelte
  5. Vergleich von Svelte mit React

Was ist Svelte?

Svelte is a way of writing user interface components [...] that users see and interact with in their browsers.

Svelte

/ˈsvɛlt/

schlank

=

Svelte

/ˈsvɛlt/

Svelte

/ˈsvɛlt/

schlank

=

anmutig

Svelte ist ein Framework

Svelte ist ein Framework

für die Erstellung von deklarativen und wiederverwendbaren Komponenten, die den Zustand und das Verhalten von UI-Elementen umfassen

Logo
Register
Home
E-Mail
Submit
Logo
Register
Home
E-Mail
Submit

<Header>

<Navbar>

<Form>

<App>

Logo
Register
Home

<Header>

<Navbar>

<Form>

<App>

<NavItem>

<NavItem>

<Logo>

E-Mail
Submit

<Label>

<Input>

<Button>

Svelte ist ein Compiler

<h1>Simple Component</h1>

Component.svelte

<h1>Simple Component</h1>
<h1>Simple Component</h1>
<h1>Simple Component</h1>
import { SvelteComponent, detach, element, init,
         insert, noop, safe_not_equal } from "svelte/internal";

function create_fragment(ctx) {
  let h1;

  return {
    c() {
      h1 = element("h1");
      h1.textContent = "Simple Component";
    },
    m(target, anchor) {
      insert(target, h1, anchor);
    },
    p: noop,
    i: noop,
    o: noop,
    d(detaching) {
      if (detaching) detach(h1);
    },
  };
}

class SimpleComponent extends SvelteComponent {
  constructor(options) {
    super();
    init(this, options, null, create_fragment, safe_not_equal, {});
  }
}

export default SimpleComponent;
<h1>Simple Component</h1>
import { [...] } from "svelte/internal";
function create_fragment(ctx) {
  let h1;  
  return {
    c() {
      h1 = element("h1");
      h1.textContent = "Simple Component";
    },
    m(target, anchor) {
      insert(target, h1, anchor);
    },
    p: noop, i: noop, o: noop,
    d(detaching) { if (detaching) detach(h1); },
  };
}
class Component extends SvelteComponent {
  constructor(options) {
    super();
    init(this, options, null, create_fragment, 
		safe_not_equal, {});
  }
}
export default Component;

Component.js

<h1>Simple Component</h1>
import Component from "./Component.js";

const component = new Component({
  target: document.body,
});

export default component;

main.js

Die Ursprünge von Svelte

Die Ursprünge von Svelte

  • Erstellt im Jahr 2016 von Rich Harris, dem Schöpfer von Rollup
     
  • Beeinflusst von Vorläufern wie React, Vue oder Angular
     
  • Begann ab Version 3.0 im Jahr 2019 an Bedeutung zu gewinnen
     
  • Konstant hohe Ränge in Stack Overflow und State of JS-Umfragen

Was bietet Svelte an?

  • Nah an der Webplattform – HTML, CSS und JS sind schon die halbe Miete
     
  • Vollständig ausgestattet – Lösungen für Zustandsmanagement, Transitionen und mehr
     
  • Großartige Leistung out-of-the-box – keine Abstraktionen wie ein virtuelles DOM erforderlich
     
  • Kleinstmögliche Bundle-Größe – es gibt keine Laufzeitbibliothek im üblichen Sinne
     
  • Ein ausgeklügeltes Reaktivitätsmodell – (fast) alles in Svelte ist reaktiv

Was bietet Svelte an?

Die Anatomie einer Svelte-Komponente

<h1>Simple Component</h1>
<h1>Simple Component</h1>
<script>
  let text = "Simple Component";
</script>

<h1>{text}</h1>
<script>
  let text = "Simple Component";
</script>

<h1>{text}</h1>
<script>
  let text = "Simple Component";
</script>

<h1>{text}</h1>

<style>
  h1 {
    color: blue;
  }
</style>
<script>
  // State & Behavior
</script>

<!-- Declarative Markup -->

<style>
  /* Scoped Component Styles */
</style>

Beispiel-Komponenten

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

<output>{counter}</output>

<button>Decrease</button>
<button>Increase</button>
<script>
  let counter = 0;
</script>

<output>{counter}</output>

<button>Decrease</button>
<button>Increase</button>
<script>
  let counter = 0;
  
  const inc = () => counter++;
  const dec = () => counter--;
</script>

<output>{counter}</output>

<button on:click={dec}>Decrease</button>
<button on:click={inc}>Increase</button>
<script>
  let notes = [];
</script>

<p>
  <button>Add New Note</button>
</p>

<ul>
  <li>Note 1 <button>&cross;</button></li>
  <li>Note 2 <button>&cross;</button></li>
  <li>Note 3 <button>&cross;</button></li>
</ul>
<script>
  let notes = [];
</script>

<p>
  <button>Add New Note</button>
</p>

<ul>
  <li>Note 1 <button>&cross;</button></li>
  <li>Note 2 <button>&cross;</button></li>
  <li>Note 3 <button>&cross;</button></li>
</ul>
<script>
  let notes = [];
</script>

<p>
  <button>Add New Note</button>
</p>

<ul>
  {#each notes as note}
    <li>
      {note} 
      <button>&cross;</button>
    </li>
  {:else}
    No notes yet.
  {/each}
</ul>
<script>
  let notes = [];
</script>

<p>
  <button>Add New Note</button>
</p>

<ul>
  {#each notes as note}
    <li>
      {note} 
      <button>&cross;</button>
    </li>
  {:else}
    No notes yet.
  {/each}
</ul>
<script>
  let notes = [];
  let addMode = false;
</script>

<p>
  {#if addMode}
    <input placeholder="New note text..." />
    <button>Add</button>
  {:else}
    <button on:click={() => (addMode = true)}>Add New Note</button>
  {/if}
</p>
<ul>
  {#each notes as note}
    <li>
      {note} 
      <button>&cross;</button>
    </li>
  {:else}
    No notes yet.
  {/each}
</ul>
<script>
  let notes = [];
  let addMode = false;
</script>

<p>
  {#if addMode}
    <input placeholder="New note text..." />
    <button>Add</button>
  {:else}
    <button on:click={() => (addMode = true)}>Add New Note</button>
  {/if}
</p>
<ul>
  {#each notes as note}
    <li>
      {note} 
      <button>&cross;</button>
    </li>
  {:else}
    No notes yet.
  {/each}
</ul>
<script>
  let notes = [], addMode = false;
  let newNote = "";

  function addNote {
    notes = [...notes, newNote];
    newNote = "";
    addMode = false;
  }
</script>

<p>
  {#if addMode}
    <input placeholder="New Note Text..." bind:value={newNote} />
    <button on:click={addNote}>Add</button>
  {:else}
    <button on:click={() => (addMode = true)}>Add New Note</button>
  {/if}
</p>

<ul>
  {#each notes as note}
    <li>
      {note}
      <button on:click={() => (notes = notes.filter((n) => n != note))}>
        &cross;
      </button>
    </li>
  {:else}
    No notes yet.
  {/each}
</ul>

Kurze Zusammenfassung

Lokaler State

State-Variablen halten den Komponentenzustand und sind standardmäßig reaktiv. Reaktivität wird durch Zuweisung, nicht durch Mutation ausgelöst.

Event Listeners

Event-Listener werden über die Direktive on:eventname angehängt und reagieren auf reguläre DOM-Ereignisse.

Logik-Blöcke

Logik-Blöcke steuern den Programmfluss innerhalb des Svelte-Markups. Sie kommen in verschiedenen Varianten vor, wie z.B. {#if} oder {#each}.

Komposition von Komponenten in Svelte

Supporter Registration

Register

E-mail...

Name...

Supporters

Supporter Registration

Register

E-mail...

Name...

Supporters

App Shell

Props

Events

Callbacks

<!-- APP SHELL -->
<script>
  import Supporters from "./Recipients.svelte";
  import Registration from "./Registration.svelte";

  let supporters = [];
</script>

<Registration
  on:register={({ detail }) => 
    (supporters = [...supporters, detail.supporter])
  }
/>
<Supporters {supporters} />
<!-- APP SHELL -->
<script>
  import Supporters from "./Recipients.svelte";
  import Registration from "./Registration.svelte";

  let supporters = [];
</script>

<Registration
  on:register={({ detail }) => 
    (supporters = [...supporters, detail.supporter])
  }
/>
<Supporters {supporters} />
<!-- SUPPORTERS -->
<script>
  export let supporters = [];
</script>

<h3>Registered Supporters</h3>
<ul>
  {#each supporters as supporter}
    <li>
      {supporter.name} 
      (<a href="mailto:{supporter.email}">
        {supporter.email}
      </a>)
    </li>
  {:else}
    No supporters yet.
  {/each}
</ul>
<!-- APP SHELL -->
<script>
  import Supporters from "./Recipients.svelte";
  import Registration from "./Registration.svelte";

  let supporters = [];
</script>

<Registration
  on:register={({ detail }) => 
    (supporters = [...supporters, detail.supporter])
  }
/>
<Supporters {supporters} />
<!-- SUPPORTERS -->
<script>
  export let supporters = [];
</script>

<h3>Registered Supporters</h3>
<ul>
  {#each supporters as supporter}
    <li>
      {supporter.name} 
      (<a href="mailto:{supporter.email}">
        {supporter.email}
      </a>)
    </li>
  {:else}
    No supporters yet.
  {/each}
</ul>
<!-- REGISTRATION -->
<script>
  import { createEventDispatcher } from "svelte";

  const dispatch = createEventDispatcher();

  function submit({ target }) {
    const formData = new FormData(target);
    const name = formData.get("name");
    const email = formData.get("email");
    dispatch("register", { name, email });
    target.reset();
  }
</script>

<h3>Supporter Registration</h3>
<form on:submit|preventDefault={submit}>
  <input type="email" name="email" placeholder="E-Mail" required />
  <input type="text" name="name" placeholder="Name" required />
  <button type="submit">Register</button>
</form>

Svelte Stores Verwenden

“A store is simply an object with a subscribe method that allows interested parties to be notified whenever the store value changes.”

<script>
  import { onDestroy } from "svelte";
  import { writable } from "svelte/store";

  let counterValue = 0;
  let counter = writable(counterValue);

  const unsubscribe = counter.subscribe((value) => (counterValue = value));

  onDestroy(unsubscribe);
</script>

<output>{counterValue}</output>

<button on:click={() => counter.update((v) => v - 1)}>decrement</button>
<button on:click={() => counter.update((v) => v + 1)}>increment</button>
<script>
  import { writable } from "svelte/store";
  let counter = writable(0);
</script>

<output>{$counter}</output>

<button on:click={() => $counter--}>decrement</button>
<button on:click={() => $counter++}>increment</button>

Kurze Zusammenfassung

Komponenten-Props

Props werden verwendet, um Daten von einer Eltern- an eine Kindkomponente zu übergeben. Sie können jeden gültigen JavaScript-Datentyp halten.

Komponenten-Events

Events werden verwendet, um Daten von einer Kind- an eine Eltern-Komponente zu übergeben. Sie werden über die Direktive on:name verwendet.

Stores

Stores sind Objekte, die einen Wert halten und abonniert werden können. Sie sind überall in der App verwendbar und erfahren besondere Unterstützung in Svelte.

Reaktive Goodies in Svelte

depends on

State Variable

Markup

Svelte Component

depends on

State Variable

Markup

Svelte Component

depends on

State Variable

depends on

State Variable

Markup

Svelte Component

State Variable

depends on

State Variable

depends on

<script>
  let counter = 0;

  const inc = () => counter++;
  const dec = () => counter--;

  $: doubled = counter * 2;
  $: sqrt = Math.sqrt(doubled).toFixed(2);
</script>

<output>{counter} * 2 = {doubled}</output>
<output>sqrt({doubled}) = {sqrt}</output>

<button on:click={dec}>Decrease</button>
<button on:click={inc}>Increase</button>

depends on

Store

Any Part

Svelte Application

depends on

Store

Any Part

Svelte Application

depends on

Store

<script>
  import { counter, doubled, sqrt } from './stores.js';
</script>

<output>{$counter} * 2 = {$doubled}</output>
<output>sqrt({$doubled}) = {$sqrt.toFixed(2)}</output>

<button on:click={counter.dec}>Decrease</button>
<button on:click={counter.inc}>Increase</button>
import { derived, writable } from "svelte/store";

const { subscribe, update } = writable(0);

export const counter = {
  subscribe,
  inc: () => update((c) => c + 1),
  dec: () => update((c) => c - 1),
};

export const doubled = derived(counter, (c) => c * 2);
export const sqrt = derived(doubled, (c) => Math.sqrt(c));

Vergleich von Svelte mit React

Weniger Code

import React, { useState } from "react";

export default function Summing() {
  const [operand1, setOperand1] = useState(0);
  const [operand2, setOperand2] = useState(0);

  return (
    <>
      <h1>Summing</h1>
      <input
        type="number"
        value={operand1}
        onChange={(e) => setOperand1(Number(e.target.value))}
      />
      +
      <input
        type="number"
        value={operand2}
        onChange={(e) => setOperand2(Number(e.target.value))}
      />
      =
      <strong>{operand1 + operand2}</strong>
    </>
  );
}
<script>
  let operand1 = 0;
  let operand2 = 0;
</script>

<h1>Summing</h1>
<input type="number" bind:value={operand1} />
+
<input type="number" bind:value={operand2} />
=
<strong>{operand1 + operand2}</strong>
import React, { useState } from "react";

export default function Summing() {
  const [operand1, setOperand1] = useState(0);
  const [operand2, setOperand2] = useState(0);

  return (
    <>
      <h1>Summing</h1>
      <input
        type="number"
        value={operand1}
        onChange={(e) => setOperand1(Number(e.target.value))}
      />
      +
      <input
        type="number"
        value={operand2}
        onChange={(e) => setOperand2(Number(e.target.value))}
      />
      =
      <strong>{operand1 + operand2}</strong>
    </>
  );
}
<script>
  let operand1 = 0;
  let operand2 = 0;
</script>

<h1>Summing</h1>
<input type="number" bind:value={operand1} />
+
<input type="number" bind:value={operand2} />
=
<strong>{operand1 + operand2}</strong>
import React, { useState } from "react";

export default function Summing() {
  const [operand1, setOperand1] = useState(0);
  const [operand2, setOperand2] = useState(0);

  return (
    <>
      <h1>Summing</h1>
      <input
        type="number"
        value={operand1}
        onChange={(e) => setOperand1(Number(e.target.value))}
      />
      +
      <input
        type="number"
        value={operand2}
        onChange={(e) => setOperand2(Number(e.target.value))}
      />
      =
      <strong>{operand1 + operand2}</strong>
    </>
  );
}
<script>
  let operand1 = 0;
  let operand2 = 0;
</script>

<h1>Summing</h1>
<input type="number" bind:value={operand1} />
+
<input type="number" bind:value={operand2} />
=
<strong>{operand1 + operand2}</strong>
import React, { useState } from "react";

export default function Summing() {
  const [operand1, setOperand1] = useState(0);
  const [operand2, setOperand2] = useState(0);

  return (
    <>
      <h1>Summing</h1>
      <input
        type="number"
        value={operand1}
        onChange={(e) => setOperand1(Number(e.target.value))}
      />
      +
      <input
        type="number"
        value={operand2}
        onChange={(e) => setOperand2(Number(e.target.value))}
      />
      =
      <strong>{operand1 + operand2}</strong>
    </>
  );
}
<script>
  let operand1 = 0;
  let operand2 = 0;
</script>

<h1>Summing</h1>
<input type="number" bind:value={operand1} />
+
<input type="number" bind:value={operand2} />
=
<strong>{operand1 + operand2}</strong>
import React, { useState } from "react";

export default function Summing() {
  const [operand1, setOperand1] = useState(0);
  const [operand2, setOperand2] = useState(0);

  return (
    <>
      <h1>Summing</h1>
      <input
        type="number"
        value={operand1}
        onChange={(e) => setOperand1(Number(e.target.value))}
      />
      +
      <input
        type="number"
        value={operand2}
        onChange={(e) => setOperand2(Number(e.target.value))}
      />
      =
      <strong>{operand1 + operand2}</strong>
    </>
  );
}
<script>
  let operand1 = 0;
  let operand2 = 0;
</script>

<h1>Summing</h1>
<input type="number" bind:value={operand1} />
+
<input type="number" bind:value={operand2} />
=
<strong>{operand1 + operand2}</strong>

Nähe zur Plattform

import React, { useRef } from "react";
import tippy from "tippy.js";

export default function App() {
  const tippyRef = useRef();

  if (tippyRef != null) {
    tippy(tippyRef.current, {
      content: "Tippy Tooltip!",
    });
  }

  return (
    <>
      <button ref={tippyRef}>Tippy Button</button>
    </>
  );
}
<script>
  import tippy from "tippy.js";
</script>

<button use:tippy={{ content: 'Tippy Tooltip!' }}>
  Tippy Button!
</button>
import React, { useRef } from "react";
import tippy from "tippy.js";

export default function App() {
  const tippyRef = useRef();

  if (tippyRef != null) {
    tippy(tippyRef.current, {
      content: "Tippy Tooltip!",
    });
  }

  return (
    <>
      <button ref={tippyRef}>Tippy Button</button>
    </>
  );
}
<script>
  import tippy from "tippy.js";
</script>

<button use:tippy={{ content: 'Tippy Tooltip!' }}>
  Tippy Button!
</button>
import React, { useRef } from "react";
import tippy from "tippy.js";

export default function App() {
  const tippyRef = useRef();

  if (tippyRef != null) {
    tippy(tippyRef.current, {
      content: "Tippy Tooltip!",
    });
  }

  return (
    <>
      <button ref={tippyRef}>Tippy Button</button>
    </>
  );
}
<script>
  import tippy from "tippy.js";
</script>

<button use:tippy={{ content: 'Tippy Tooltip!' }}>
  Tippy Button!
</button>

Lernkurve

Lernkurve

  • HTML, CSS, JS sind bereits die halbe Miete
     
  • Syntax-Erweiterungen ähnlich wie Template-Engines
     
  • Spezifika sind leicht zu erlernen

Feature-Vergleich

Svelte React
CSS Support
Transitionen
Animationen
State Management /
Deklarativer Zugriff auf window, head oder body
Nutzbar ohne Build-Prozess /
Großes Ökosystem /
Gefördert durch großes Unternehmen