Mounting and Effect Rune
Learning Objectives
- You know the terms server-side rendering, hydration, mounting, and unmounting.
- You know how to use Svelte’s
$effectrune to run code when a component is mounted. - You know how to show a loading indicator while waiting for asynchronous functionality to complete.
Rendering and Component Mount
When an application is opened in a browser, it goes through several steps before it becomes fully interactive. In SvelteKit, this involves server-side rendering (SSR) and hydration at the framework level, and mounting at the component level.
Server-side rendering and hydration
When a browser requests a page in SvelteKit, two key steps usually happen before the page becomes interactive:
-
Server-side rendering (SSR): Entire pages or layouts (depending on configuration) can be rendered on the server and sent to the browser as HTML. This means the server generates an initial HTML representation of the page (often including data fetched on the server) before it is sent.
-
Hydration: Once the browser receives the server-rendered HTML, the initial server-rendered HTML representation can be displayed immediately. This is followed by SvelteKit loading any necessary JavaScript. SvelteKit then hydrates the existing DOM: attaching event listeners, reactivity, and component logic to the already-rendered nodes. Hydration typically reuses the DOM rather than discarding it, though it may adjust mismatches. After hydration, components become interactive.
It is possible to disable SSR for a page or layout in SvelteKit by exporting a constant named ssr with the value false from a +page.js, +layout.js, +page.server.js, or +layout.server.js file. If SSR is disabled, the server sends a minimal HTML shell and JavaScript bundle, and the browser mounts the page entirely client-side instead of hydrating.
Mounting components
At the component level, mounting refers to the moment when a component’s DOM is inserted into the document and becomes part of the running app.
The lifecycle of a component looks like this:
- Creation: the component instance is created — its variables and logic exist, but no DOM yet.
- Mounting: Svelte generates DOM nodes and attaches them to the page.
- Updating: the component reacts to state or prop changes.
- Destroying: the component is removed from the DOM.
These two levels, framework-level and component-level jointly lead to a fully interactive page. The flow of both are summarized in Figure 1 below.
Svelte has a few mechanisms that let you run code at specific points in this lifecycle. These allow us to safely interact with the DOM or perform side effects such as loading data from an API when a component is mounted or unmounted.
Effect Rune
The $effect rune allows executing code when a component is mounted or when its dependencies such as reactive variables change.
Runes are special Svelte constructs that allow you to execute code at specific points in the component lifecycle. They are similar to lifecycle methods in other frameworks, such as React’s
useEffector Vue’smountedhook. The$statefunction that we use to create reactive variables is also a rune.
Running code on mount
The $effect rune takes a function, which runs right after the component has been mounted to the DOM. As an example, the following component uses the $effect rune to change the text of a paragraph.
<script>
let text = $state("Hello, world!");
$effect(() => {
text = "Component mounted!";
});
</script>
<p>{text}</p>
When the above component is mounted, the paragraph updates to show “Component mounted!” instead of “Hello, world!”.
Asynchronous effects
The function that is passed to $effect can also be asynchronous. This makes it straightforward to run code that takes some time to complete, such as fetching data from an API or waiting for a timeout. In the following example, there is a wait of 5 seconds before the text is changed.
<script>
let text = $state("Hello, world!");
$effect(async () => {
// extra wait for demonstration
await new Promise(resolve => setTimeout(resolve, 5000));
text = "Waited for 5 seconds to show this!";
});
</script>
<p>{text}</p>
If you were to place the await outside of the $effect rune, the component would not render until the wait was over. This is because the entire script block is executed before the component is rendered.
You can try this by first trying the above component locally to see how it works. Then, try out the following example that does the same but without the $effect rune.
<script>
let text = $state("Hello, world!");
// extra wait for demonstration
await new Promise(resolve => setTimeout(resolve, 5000));
text = "Waited for 5 seconds to show this!";
</script>
<p>{text}</p>
Effect and dependencies
The $effect rune automatically registers reactive values that are synchronously used inside the function as dependencies. When the dependencies change, the function is re-run. For example, in the following component, the $effect rune updates the doubled variable whenever the count variable changes.
<script>
let count = $state(0);
let doubled = $state(0);
$effect(() => {
doubled = count * 2;
});
</script>
<button onclick={() => count++}>Increment count</button>
<p>Count: {count}</p>
<p>Doubled: {doubled}</p>
In the above example, the $effect rune automatically registers count as a dependency because it is used inside the function. Whenever count changes, the function is re-run and doubled is updated.
The effect rerun is limited to dependencies changing, not to changes in dependency properties. For example, if you have an object as a dependency and you change one of its properties, the effect will not re-run. This means, for example, that the effect is not rerun if a new value is added to an array that is a dependency.
Effect and Fetch
The Effect rune is in particular useful when working with the Fetch API to load data from a server when the component is mounted. For example, if we want to fetch a list of todos from an API when the component is mounted, we can use the $effect rune to call the API and update the state with the fetched data.
In the example below, we use the Fetch API to retrieve a list of todos from the API at https://wsd-todos-api.deno.dev/todos when the component is mounted. Now, the user would not have to explicitly press a button to load the content.
<script>
let todos = $state([]);
const fetchTodos = async () => {
const response = await fetch(
"https://wsd-todos-api.deno.dev/todos"
);
todos = await response.json();
};
$effect(() => {
fetchTodos();
});
</script>
<h1>Todos</h1>
<ul>
{#each todos as todo}
<li>{todo.name}</li>
{/each}
</ul>
Waiting for Data to Load
When working with lifecycles and loading data from an API, it is common to want to show some kind of loading indicator to the user while waiting for the data to be loaded. This is particularly important if the data takes a long time to load, as it can otherwise lead to a poor user experience.
There are two main ways to show loading indicators in Svelte: using conditional rendering with an if block, or using Svelte’s await block.
Conditional rendering
Conditional rendering involves using an if block to show different content depending on whether the data has been loaded or not. For this, we would need a separate state variable that indicates whether the data has been loaded or not.
The following example outlines how the todo list example from above could be modified to show a loading indicator while the todos are being fetched. In this case, we use a loading variable that is set to true when the fetching starts and set to false when the fetching is complete. The if block is then used to show either the loading message or the list of todos depending on the value of the loading variable.
<script>
let todos = $state([]);
let loading = $state(true);
const fetchTodos = async () => {
loading = true;
const response = await fetch(
"https://wsd-todos-api.deno.dev/todos"
);
todos = await response.json();
// extra wait for demonstration
await new Promise(resolve => setTimeout(resolve, 2500));
loading = false;
};
$effect(() => {
fetchTodos();
});
</script>
<h1>Todos</h1>
{#if loading}
<p>Loading todos...</p>
{:else}
<ul>
{#each todos as todo}
<li>{todo.name}</li>
{/each}
</ul>
{/if}
With the above, the message “Loading todos…” is shown to the user while the todos are being fetched. Once the todos have been fetched, the message is replaced with the list containing the todos.
Await block
Svelte has an await block with three parts that is specifically designed for handling asynchronous functionality.
The first part is #await that is given the promise that is awaited and that contains the content that is shown while waiting for the promise to be resolved or rejected. The second part is :then that contains the content that is shown when the promise is resolved. The third part is :catch that contains the content that is shown when the promise is rejected.
The :then and :catch are given a variable that contains the resolved value or the error, respectively.
The following outlines the basic syntax of the await block.
{#await promise}
show while waiting
{:then variable}
show contents variable which corresponds to the resolved promise
{:catch error}
show if the promise is rejected
{/await}
The following example shows how the todo list example from above could be modified to use an await block to show a loading indicator while the todos are being fetched. In this case, we use a todoPromise variable that is set to the promise returned by the fetchTodos function. The await block is then used to show either the loading message, the list of todos, or an error message depending on the state of the promise.
<script>
let todoPromise = $state({});
const fetchTodos = async () => {
const response = await fetch(
"https://wsd-todos-api.deno.dev/todos"
);
// extra wait for demonstration
await new Promise(resolve => setTimeout(resolve, 2500));
return await response.json();
};
$effect(() => {
todoPromise = fetchTodos();
});
</script>
<h1>Todos</h1>
{#await todoPromise}
<p>Loading todos...</p>
{:then todos}
<ul>
{#each todos as todo}
<li>{todo.name}</li>
{/each}
</ul>
{:catch error}
<p>Error when retrieving todo: {error.message}</p>
{/await}
Summary
In summary:
- Server-side rendering (SSR) is when the server generates HTML for a page before sending it to the browser, while hydration is the process of attaching JavaScript functionality to that HTML to make it interactive.
- Components are mounted when their DOM is inserted into the document, making them part of the running application.
- Svelte has the
$effectrune that allows you to run code when a component is mounted. - The
$effectrune is useful for running code that needs to be run only on the browser when the component is mounted, such as retrieving data from an API.