Understanding Svelte actions
Published on November 24, 2020Actions 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
.
- onMount fires when the component is added to the dom
- onDestroy is fired when the component is removed from the dom
- beforeUpdate runs before the markup is updated
- afterUpdate runs after the markup has been updated
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
}
}
}
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!