Actions are one of Svelte’s less commonly used features. An action allows you to run a function when an element is added to the DOM. While that sounds simple, an action used in the right way can greatly simplify your code and allow you to reuse bits of logic without creating an entirely separate component.
In this post, I’ll give two examples where a Svelte action would be useful and show why an action is the right tool for the job.
Let’s start with the following Svelte component. We have some static text with an edit button next to it. When the edit button is clicked, a text field is revealed. Typing in the field updates the text, and you can confirm to save your changes.
<script> let name = 'world'; let editing = false; function toggleEdit() { editing = !editing } </script> <p> Name: {name} </p> {#if editing} <label> Name <input type="text" bind:value={name}> </label> {/if} <button on:click={toggleEdit}> {editing ? 'Confirm' : 'Edit'} </button>
This UI is a little annoying, since you need to click (or tab into) the edit field after clicking the edit button. It would be a better experience if it was automatically focused, so you could start typing right away. How can we do that?
bind:this
If you’re familiar with binding to DOM elements in Svelte, you might think of doing something like this:
<script> let name = 'world'; let editing = false; let input; function toggleEdit() { editing = !editing if (editing) { input.focus(); } } </script> <p> Name: {name} </p> {#if editing} <label> Name <input bind:this={input} type="text" bind:value={name}> </label> {/if} <button on:click={toggleEdit}> {editing ? 'Confirm' : 'Edit'} </button>
However, if you try to run that code, you get an error in the console:
Uncaught TypeError: input is undefined
This is because the input is not added to the DOM yet, so you can’t focus it after setting editing
to true
.
Instead, we need to call Svelte’s tick function
, which returns a promise that resolves when Svelte has finished applying any pending state changes. Once tick
resolves, the DOM will be updated and we can focus the input.
function toggleEdit() { editing = !editing if (editing) { tick().then(() => input.focus()); } }
That works, but it doesn’t feel very intuitive. It’s also not very reusable — what if we want to apply this behavior to other inputs?
Another option is to move the input into its own component, and focus the input when that component mounts. Here’s what that looks like:
<script> export let value; export let label; let input; import { onMount } from 'svelte'; onMount(() => { input.focus(); }); </script> <label> {label} <input type="text" bind:this={input} bind:value> </label>
Then it can be used in the parent component, like so:
{#if editing} <Input bind:value={name} label="name" /> {/if}
However, with this approach you have to incur the cost of creating a new component, which you didn’t need to do otherwise. If you want to apply this behavior to another input element, you would need to make sure to expose props for every attribute that is different.
You are also limited to input elements with this method, and would need to reimplement this behavior if you wanted to apply it to another element.
Though these are all viable solutions, it feels like you’re having to work around Svelte instead of with it. Thankfully, Svelte has an API to make this sort of thing easier: actions.
An action is just a function. It takes a reference to a DOM node as a parameter and runs some code when that element is added to the DOM.
Here’s a simple action that will call focus on the node. We don’t have to call tick
this time because this function will only run when the node already exists.
function focusOnMount(node) { node.focus(); }
We can then apply it to a node with the use:
directive.
{#if editing} <label> Name <input use:focusOnMount type="text" bind:value={name}> </label> {/if}
That’s a lot cleaner! This is just a few lines of code to solve the same problem we were dealing with before, and it’s reusable without needing to create a separate component. It’s also more composable, since we can apply this behavior to any DOM element that has a focus
method.
You can see the final demo in this Svelte REPL.
Actions are also great when you want to integrate with a vanilla JavaScript library that needs a reference to a specific DOM node. This is another strength of Svelte — while the Svelte-specific ecosystem is still growing, it’s still easy to integrate with the vast array of vanilla JS packages!
Let’s use the tooltip library Tippy.js as an example. We can pass a DOM element to initialize Tippy on that node, and also pass an object of parameters.
For example, here’s how we can add a tooltip using vanilla JS:
import tippy from 'tippy.js'; tippy(document.getElementById('tooltip'), { content: 'Hello!' });
We can use a Svelte action to run this code so we have a reference to the node without calling document.getElementById
. Here’s what that might look like:
function tooltip(node) { let tip = tippy(node, { content: 'Hello!' }); }
And it can be used on an element like so:
<button use:tooltip> Hover me </button>
But how do we customize the properties we use to initialize the tooltip? We don’t want it to be the same for every use of the action.
Actions can also take parameters as a second argument, which means we can easily customize the tooltip and allow consumers to pass in the parameters they want.
function tooltip(node, params) { let tip = tippy(node, params); }
And here’s how you use it on an element:
<button use:tooltip={{ content: 'New message' }}> Hover me </button>
Note the double curly brackets. You put the parameters you want to pass to the action inside the curly brackets. Since we’re passing an object to this action, there are two sets of curly brackets: one to wrap the parameters and one for the parameter object itself.
This works, but there’s a few problems:
Thankfully, actions can return an object with update
and destroy
methods that handle both these problems.
The update
method will run whenever the parameters you pass to the action change, and the destroy
method will run when the DOM element that the action is attached to is removed. We can use the Tippy setProps
function to update the parameters, and destroy
to remove the element when we’re done.
Here’s what the action looks like if we implement these methods:
function tooltip(node, params) { let tip = tippy(node, params); return { update: (newParams) => { tip.setProps(newParams); }, destroy: () => { tip.destroy(); } } }
This allows us to write a more complicated example that updates the placement and message of the tooltip after initial creation:
<script> import tippy from 'tippy.js'; function tooltip(node, params) { let tip = tippy(node, params); return { update: (newParams) => { tip.setProps(newParams); }, destroy: () => { tip.destroy(); } } } const placements = ['top', 'right', 'bottom', 'left']; let selectedPlacement = placements[0]; let message = "I'm a tooltip!"; </script> <label for="placement">Placement</label> <select bind:value={selectedPlacement} id="placement"> {#each placements as placement} <option>{placement}</option> {/each} </select> <label>Message <input bind:value={message} type="text"></label> <button use:tooltip={{ content: message, placement: selectedPlacement }}> Hover me </button>
You can find the final example in this Svelte REPL.
As with the example before, we didn’t need actions to be able to do this. We could also attach the tooltip when the component mounts and update the parameters using reactive statements. Here’s what that might look like:
<script> import tippy from 'tippy.js'; import { onMount, onDestroy } from 'svelte'; let button; let tip; onMount(() => { tip = tippy(button, { content: message, placement: selectedPlacement}); }); $: if (tip) { tip.setProps({ content: message, placement: selectedPlacement }); } onDestroy(() => { tip.destroy(); }); const placements = ['top', 'right', 'bottom', 'left']; let selectedPlacement = placements[0]; let message = "I'm a tooltip!"; </script> <label for="placement">Placement</label> <select bind:value={selectedPlacement} id="placement"> {#each placements as placement} <option>{placement}</option> {/each} </select> <label>Message <input bind:value={message} type="text"></label> <button bind:this={button}> Hover me </button>
This approach is totally valid. However, it is less reusable across multiple components and becomes tricky if the tooltip element is conditionally rendered or in a loop.
You might also think of creating a component like <TooltipButton>
to encapsulate the logic. This will also work, though it limits you to one type of element. Implementing it as an action lets you apply the tooltip to any element, not just a button.
Actions are a very powerful Svelte feature. Now that you’re familiar with them, make sure you check out the official tutorial and docs to see other ways to use actions. They aren’t always the right solution — many times, it is better to encapsulate the behavior in other ways, such as in a separate component or with a simple event handler. However, there are times like the examples above where they make your component code much cleaner and more reusable.
There is also an open RFC to add inbuilt actions to Svelte, similar to how Svelte includes inbuilt transitions. As part of that RFC, the community created a POC library with some commonly-used actions such as longpress
, clickOutside
, and lazyload
.
You may also be interested in a post I wrote last year on using actions to detect when a sticky-positioned element becomes stuck to the viewport.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]