Understanding Svelte actions

Published on November 24, 2020

Actions are one of those Svelte concepts that can be a bit hard to grasp initially. They often are offered as solutions to problems (and they can be) but for beginners they feel a bit magical. But there is a relatively easy way to try to understand actions and that is by comparing them to regular components.

Let’s first look at what an action usually is explained as:

A function that is called when an element is created, taking the element and optional parameters as arguments.

That is indeed not always very helpfull. The most basic example on it’s self is also not worth anything to explain it.

function action(node) {
  console.log('the element has mounted')
}

In this post I would like to try to demystify the three stages of actions: creating, destroying and updating, by comparing them to something Svelte user understand better and that is regular components.

Component Lifecycle Methods

Components come with four lifecycle methods: onMount, onDestroy, beforeUpdate and afterUpdate.

Let’s say we have an oversimplified component like this:

<script>
  import { afterUpdate, onDestroy, onMount } from 'svelte'

  export let prop 

  afterUpdate(() => console.log('property did update', prop))
  onDestroy(() => console.log('component did unmount'))
  onMount(() => console.log('component did mount'))    
</script>

<div>
  <slot></slot>
</div>

Note that I left beforeUpdate here, that is on purpose

This component will simply log a message whenever it’s mounted, destroyed or updated. It can be used very simply as

<MyComponent prop="1">Content goes here</MyComponent>

And whenever prop changes it will log out the message.

Reusing the component

Now, if we imagine that we would like to reuse this behaviour but with other properties, actually useful functionalities and more, we cannot easily do this without starting to setup strange construction to pass along arbitrary props, behaviours and creating a complex maze of slots and contexts to encapsulate other functionality.

Enter Actions

Let’s see how actions can help with making this easier by making a new action, aptly named myaction and using it as follows:

<div use:myaction>Content goes here</div>

The first thing we would like to mimick is the onMount. This is easy, because as we know from the definition of an action, it is a function executed when an element is added to the DOM. That would gives us the following:

function myaction(node) {
  console.log('element is mounted')
}

We would also like to mimick the onDestroy event. To do this we can have the action return an object with a destroy method defined, this method will be called when the element is removed from the DOM.

function myaction(node) {
  console.log('element is mounted')

  return {
    destroy() {
      console.log('element is unmounted')
    }
  }
}

For the last option, afterUpdate we have to quickly go back to how we utilize the action. When adding the action to the element we can also provide parameters:

<div use:myaction={prop}>Content goes here</div>

In the action we can access the parameter given as the second argument of the function function(node, params).

function myaction(node, params) {
  console.log(params) // params will be the same as 'prop' in our case
}

Now in order to react to changes in the params, we can take our return object and add a update function to it that takes in the new values.

function myaction(node, params) {
  console.log('element is mounted')

  return {
    destroy() {
      console.log('element is unmounted')
    },
    update(new_params) {
      console.log('params have updated', new_params)
    }
  }
}

This update function will run after the markup has been updated. Note that at this point the old params are still available as the original value was entered on creation and never changes. If you want to keep track of the new value you have to reassign it:

function myaction(node, params) {
    return {
        update(new_params) {
            console.log('the old value is', params)
            console.log('the new values is', new_params)

            params = new_params
        }
    }
}
As far as I understand there is no equivalent for beforeUpdate using actions

Conclusion

Actions can be considered as a way to add lifecycle methods to individual DOM elements as opposed to the normal lifecycle events we see in components. This also explains to a degree why you cannot use actions on components: components already have another way to do this.

While there is not a 100% match between component lifecycle events and actions, it is close enough to start to understand what actions are and how to use them. If you see yourself making a component that is almost empty with just some lifecycle events and a superflat DOM existing of a simple wrapper and a slot, you might have a good candidate for an action!