Book Image

Real-World Svelte

By : Tan Li Hau
4.3 (4)
Book Image

Real-World Svelte

4.3 (4)
By: Tan Li Hau

Overview of this book

Svelte has quickly become a popular choice among developers seeking to build fast, responsive, and efficient web applications that are high-performing, scalable, and visually stunning. This book goes beyond the basics to help you thoroughly explore the core concepts that make Svelte stand out among other frameworks. You’ll begin by gaining a clear understanding of lifecycle functions, reusable hooks, and various styling options such as Tailwind CSS and CSS variables. Next, you’ll find out how to effectively manage the state, props, and bindings and explore component patterns for better organization. You’ll also discover how to create patterns using actions, demonstrate custom events, integrate vanilla JS UI libraries, and progressively enhance UI elements. As you advance, you’ll delve into state management with context and stores, implement custom stores, handle complex data, and manage states effectively, along with creating renderless components for specialized functionalities and learning animations with tweened and spring stores. The concluding chapters will help you focus on enhancing UI elements with transitions while covering accessibility considerations. By the end of this book, you’ll be equipped to unlock Svelte's full potential, build exceptional web applications, and deliver performant, responsive, and inclusive user experiences.
Table of Contents (22 chapters)
1
Part 1: Writing Svelte Components
6
Part 2: Actions
10
Part 3: Context and Stores
16
Part 4: Transitions

Reusing lifecycle functions in Svelte components

In the previous section, we learned that we can extract the calling of lifecycle functions into a function and reuse the function in other components.

Let’s look at an example. In this example, after the component is added to the screen for 5 seconds, it will call the showPopup function. I want to reuse this logic of calling showPopup in other components:

<script>
  import { onMount } from 'svelte';
  import { showPopup } from './popup';
  onMount(() => {
    const timeoutId = setTimeout(() => {
      showPopup();
    }, 5000);
    return () => clearTimeout(timeoutId);
  });
</script>

Here, I can extract the logic into a function, showPopupOnMount:

// popup-on-mount.js
import { onMount } from 'svelte';
import { showPopup } from './popup';
export function showPopupOnMount() {
  onMount(() => {
    const timeoutId = setTimeout(() => {
      showPopup();
    }, 5000);
    return () => clearTimeout(timeoutId);
  });
}

And now, I can import this function and reuse it in any component:

<script>
  import { showPopupOnMount } from './popup-on-mount';
  showPopupOnMount();
</script>

You may be wondering, why not only extract the callback function and reuse that instead?

// popup-on-mount.js
import { showPopup } from './popup';
export function showPopupOnMount() {
  const timeoutId = setTimeout(() => {
    showPopup();
  }, 5000);
  return () => clearTimeout(timeoutId);
}

Over here, we extract only setTimeout and clearTimeout logic into showPopupOnMount, and pass the function into onMount:

<script>
  import { onMount } from 'svelte';
  import { showPopupOnMount } from './popup-on-mount';
  onMount(showPopupOnMount);
</script>

In my opinion, the second approach of refactoring and reusing is not as good as the first approach. There are a few pros in extracting the entire calling of the lifecycle functions into a function, as it allows you to do much more than you can otherwise:

  • You can pass in different input parameters to your lifecycle functions.

    Let’s say you wish to allow different components to customize the duration before showing the popup. It is much easier to pass that in this way:

    <script>
      import { showPopupOnMount } from './popup-on-mount';
      showPopupOnMount(2000); // change it to 2s
    </script>
  • You can return values from the function.

    Let’s say you want to return the timeoutId used in the onMount function so that you can cancel it if the user clicks on any button within the component.

    It is near impossible to do so if you just reuse the callback function, as the value returned from the callback function will be used to register for the onDestroy lifecycle function:

    <script>
      import { showPopupOnMount } from './popup-on-mount';
      const timeoutId = showPopupOnMount(2000);
    </script>
    <button on:click={() => clearTimeout(timeoutId)} />

    See how easy it is to implement it to return anything if we write it this way:

    // popup-on-mount.js
    export function showPopupOnMount(duration) {
      let timeoutId;
      onMount(() => {
        timeoutId = setTimeout(() => {
          showPopup();
        }, duration ?? 5000);
        return () => clearTimeout(timeoutId);
      });
      return timeoutId;
    }
  • You can encapsulate more logic along with the lifecycle functions.

    Sometimes, the code in your lifecycle functions callback function does not work in a silo; it interacts with and modifies other variables. To reuse lifecycle functions like this, you must encapsulate those variables and logic into a reusable function.

    To illustrate this, let’s look at a new example.

    Here, I have a counter that starts counting when a component is added to the screen:

    <script>
      import { onMount } from 'svelte';
      let counter = 0;
      onMount(() => {
        const intervalId = setInterval(() => counter++, 1000);
        return () => clearInterval(intervalId);
      });
    </script>
    <span>{counter}</span>

    The counter variable is coupled with the onMount lifecycle functions; to reuse this logic, the counter variable and the onMount function should be extracted together into a reusable function:

    import { writable } from 'svelte/store';
    import { onMount } from 'svelte';
    export function startCounterOnMount() {
      const counter = writable(0);
      onMount(() => {
        const intervalId = setInterval(() => counter.update($counter => $counter + 1), 1000);
        return () => clearInterval(intervalId);
      });
      return counter;
    }

    In this example, we use a writable Svelte store to make the counter variable reactive. We will delve more into Svelte stores in Part 3 of this book.

    For now, all you need to understand is that a Svelte store allows Svelte to track changes in a variable across modules, and you can subscribe to and retrieve the value of the store by prefixing a $ in front of a Svelte store variable. For example, if you have a Svelte store named counter, then to get the value of the Svelte store, you would need to use the $counter variable.

    Now, we can use the startCounterOnMount function in any Svelte component:

    <script>
      import { startCounterOnMount } from './counter';
      const counter = startCounterOnMount();
    </script>
    <span>{$counter}</span>

I hope I’ve convinced you about the pros of extracting the calling of lifecycle functions into a function. Let’s try it out in an example.

Exercise 1 – Update counter

In the following example code, I want to know how many times the component has gone through the update cycle.

Using the fact that every time the component goes through the update cycle, the afterUpdate callback function will be called, I created a counter that will be incremented every time the afterUpdate callback function is called.

To help us measure only the update count of a certain user operation, we have functions to start measuring and stop measuring, so the update counter is only incremented when we are measuring:

<script>
  import { afterUpdate } from 'svelte';
  let updateCount = 0;
  let measuring = false;
  afterUpdate(() => {
    if (measuring) {
      updateCount ++;
    }
  });
  function startMeasuring() {
    updateCount = 0;
    measuring = true;
  }
  function stopMeasuring() {
    measuring = false;
  }
</script>
<button on:click={startMeasuring}>Measure</button>
<button on:click={stopMeasuring}>Stop</button>
<span>Updated {updateCount} times</span>

To reuse all the logic of the counter: – the counting of update cycles and the starting and stopping of the measurement – we should move all of it into a function, which ends up looking like this:

<script>
  import { createUpdateCounter } from './update-counter';
  const { updateCount, startMeasuring, stopMeasuring } = createUpdateCounter();
</script>
<button on:click={startMeasuring}>Measure</button>
<button on:click={stopMeasuring}>Stop</button>
<span>Updated {$updateCount} times</span>

The update counter returns an object that contains the updateCount variable and the startMeasuring and stopMeasuring functions.

The implementation of the createUpdateCounter function is left as an exercise to you, and you can check the answer at https://github.com/PacktPublishing/Real-World-Svelte/tree/main/Chapter01/01-update-counter.

We’ve learned how to extract a lifecycle function and reuse it, so let’s take it up a notch and reuse multiple lifecycle functions in the next pattern: composing lifecycle functions.