13 Svelte Concepts You Should Know

To see this in action, let’s create a basic to-do app and review 13 concepts you should know when working with Svelte.

Just like in any other modern framework, Svelte apps are components or collections of components which keep track of state (aka data), and automatically update the DOM when the data is changed by various user or server events.

1. Single File Structure

Svelte components follow the single file structure, where the JavaScript logic, the HTML template and the CSS rules are all stored under the same file.

<script lang="ts"></script>

<section></section>

<style></style>

2. Svelte Compiler

These files are then compiled into the optimized JavaScript bundles executed by the browsers.

3. Runes

Starting with Svelte 5 we can use runes to declare reactive state in our components.

<script lang="ts">
  let text = $state("");
  let tasks = $state<Task[]>([]);
</script>

These are special functions which use signals under the hood to keep track of an internal value and notify a list of subscribers whenever that value changes. Reactivity applies to both primitives and arrays or objects.

4. Derived State

What’s more interesting though is that we can derive this state in a declarative manner.

<script lang="ts">
  let text = $state("");
  let tasks = $state<Task[]>([]);
  let count = $derived(tasks.length);
  let doneCount = $derived.by(() => {
    return tasks.filter((it) => it.done).length;
  });
</script>

You can use the derived function for simple expressions, and the derived by if you need to create more complex derivations.

5. Effects

While derived functions should be free of side-effects, you can use the effect rune to execute code that has implications outside the reactivity context.

<script lang="ts">
  let text = $state("");
  let tasks = $state<Task[]>([]);
  let count = $derived(tasks.length);
  let doneCount = $derived.by(() => {
    return tasks.filter((it) => it.done).length;
  });
  $effect(() => {
    console.log(tasks.length);
  });
</script>

In our example, we might want to populate the task lists with values already stored on the server.

6. Lifecycle Hooks

The onMount lifecycle hook will run once, after the component is rendered in the DOM, and it is a good place to initialize your app state.

<script lang="ts">
  let text = $state("");
  let tasks = $state<Task[]>([]);
  let count = $derived(tasks.length);
  let doneCount = $derived.by(() => {
    return tasks.filter((it) => it.done).length;
  });
  onMount(() => {
    getTasks().forEach((it) => tasks.push(it));
  });
</script>

7. Templating

Moving to the template section, we can render data in the DOM using curly braces, and use the “each” block to iterate over lists of elements. As a good practice, we’ll associate a unique ID with each DOM entry, so that the framework can efficiently keep the DOM in sync.

<script lang="ts">
  {...}
</script>

<section>
  <h1>{doneCount} / {count} Tasks</h1>
  {#each tasks as task (task.id)}
  <input type="checkbox" bind:checked="{task.done}" />
  <h6>{task.text}</h6>
  {/each}
  <footer>
    <input value="{text}" onchange="{ev" ="" /> text = ev.target.value} />
    <button onclick="{add}">Save</button>
  </footer>
</section>

8. Data Bindings

We’ll use one-way data binding to capture the user input into the text value.

<input value="{text}" />

9. Event Listeners

The on-click event listener to save new entries in our task list.

<button onclick="{add}">Save</button>

What you might not realize is that you can simply push your new task into the list, and the framework will take care of all the derived values, effect functions and DOM updates for you.

<script lang="ts">
  function addTask(task) {
    tasks.push({
      id: crypto.randomUUID(),
      text,
      done: false,
    });
    saveTasks(tasks);
    text = "";
  }
</script>

<section>{...}</section>

We might consider adding more task related functionality

<script lang="ts">
  function addTask(task) {
    tasks.push({
      id: crypto.randomUUID(),
      text,
      done: false,
    });
    saveTasks(tasks);
    text = "";
  }

  function removeTask(id) {
    tasks = tasks.filter((task) => task.id !== id);
    saveTasks(tasks);
  }
</script>

<section>{...}</section>

10. Components

Since the Task line might grow in complexity, it might be a good idea to extract this code in a separate child component. We can then pass down the task object, and any other needed functionality as properties.

<script lang="ts">
  {...}
</script>

<section>
  <h1>{doneCount} / {count} Tasks</h1>
  {#each tasks as task (task.id)}
  <TaskLine task="{task}" onRemove="{remove}" />
  {/each}
  <footer>
    <input value="{text}" onchange="{ev" ="" /> text = ev.target.value} />
    <button onclick="{add}">Save</button>
  </footer>
</section>

By the way, you are watching ”The Snippet”, the fast-paced, no BS series where we are reading code to get better at writing code.

11. Properties

In the new task line file component, we’ll use the properties hook to retrieve the information and render that in the DOM.

<script lang="ts">
  import type { Task } from "../../services/service";

  const { task, onRemove } = $props<{
    task: Task;
    onRemove: (task: Task) => void;
  }>();
</script>

<div class="task">
  <input type="checkbox" bind:checked="{task.done}" />
  <h6>{task.text}</h6>
  <button onclick="{()" ="">onRemove(task)}>Delete</button>
</div>

12. TypeScript Support

Note that Svelte offers native TypeScript support, but you have to specify it in your script tag.

<script lang="ts">
  {...}
</script>

13. Two-Way Data Binding

Finally, for convenience reasons, we can use two-way data binding to capture the change of the task done state.

<input type="checkbox" bind:checked="{task.done}" />

If you like this fast-paced style but want a deeper dive into frontend concepts, take a look at my Yes JS course, or check out some of my youtube videos or subscribe to the newsletter.

Until next time, thank you for reading!