Use Directive
use:action runs a function on an element when that element
mounts. That function - an action - gets the element, sets up a reusable
behaviour on it, and can return cleanup that runs on unmount.
Why not just a ref?
A ref: is one variable pointing at one element in one component,
and any teardown is yours to write.
An action is one function you can attach to any number of elements in any component; it cleans up after itself, and it can react to a changing parameter.
Below, the same clickOutside action is attached to two menus.
No second ref variable, no duplicated wiring, and each document
listener is removed when its element goes away - which a listener on
document must be, or it leaks.
<script>
let fileOpen = false
let editOpen = false
// One behaviour, attached to both menus below.
function clickOutside(element, onOutside) {
const onDocumentClick = (event) => {
if (!element.contains(event.target)) onOutside()
}
document.addEventListener('click', onDocumentClick)
// Returned cleanup runs when the element unmounts
return () => document.removeEventListener('click', onDocumentClick)
}
// The parameter must be a named function, not an inline arrow
function closeFile() { fileOpen = false }
function closeEdit() { editOpen = false }
</script>
<div class="menubar">
<div class="menu" use:clickOutside={closeFile}>
<button class="menu-button" onclick="fileOpen = !fileOpen">File</button>
<show when={fileOpen}>
<ul class="menu-list">
<li>New</li>
<li>Open</li>
<li>Save</li>
</ul>
</show>
</div>
<div class="menu" use:clickOutside={closeEdit}>
<button class="menu-button" onclick="editOpen = !editOpen">Edit</button>
<show when={editOpen}>
<ul class="menu-list">
<li>Undo</li>
<li>Cut</li>
<li>Paste</li>
</ul>
</show>
</div>
</div>
<style>
.menubar {
display: flex;
gap: 0.5rem;
min-height: 12rem;
}
.menu {
position: relative;
}
.menu-button {
padding: 0.4rem 0.9rem;
border: 1px solid var(--border);
border-radius: var(--radius-sm);
background: var(--bg-elevated);
color: var(--text-primary);
font: inherit;
cursor: pointer;
}
.menu-button:hover {
border-color: var(--border-hover);
}
.menu-list {
position: absolute;
top: calc(100% + 0.35rem);
left: 0;
z-index: 1;
min-width: 8rem;
margin: 0;
padding: 0.25rem;
list-style: none;
background: var(--bg-elevated);
border: 1px solid var(--border);
border-radius: var(--radius-md);
box-shadow: var(--shadow-card);
}
.menu-list li {
list-style: none;
margin: 0;
padding: 0.35rem 0.6rem;
border-radius: var(--radius-sm);
color: var(--text-secondary);
cursor: pointer;
}
.menu-list li:hover {
background: var(--bg-hover);
color: var(--text-primary);
}
</style>Parameters
Whatever follows = becomes the action's second argument.
The example above passes a function; a plain string works too, as in
use:tooltip="Hello world". With no parameter at all, as in
use:autofocus, the action is called with just the element.
Cleanup and updates
An action can return nothing, a cleanup function (as
clickOutside does), or an object with update and
destroy.
Return the object when the parameter is reactive: update is
called with the new value every time it changes, instead of the action being
torn down and re-run.
<script>
let message = 'Hello'
function tooltip(element, text) {
element.title = text
return {
update(newText) {
// Called when parameter changes
element.title = newText
},
destroy() {
// Called when element unmounts
element.title = ''
}
}
}
</script>
<span use:tooltip={message}>Hover me</span>
<button onclick="message = 'Updated!'">Change tooltip</button>
Third-party libraries
This is where actions earn their keep.
A charting or editor library wants a real element, a way to be told the data changed, and a way to be disposed - the three parts of an action.
<script>
// Sketch, not a running demo. `Chart` stands in for any library that takes
// an element, exposes an update call, and needs disposing.
const chartOptions = {
type: 'bar',
data: {
labels: ['Mon', 'Tue', 'Wed'],
datasets: [{ data: [3, 7, 2] }]
}
}
function chart(element, options) {
const instance = new Chart(element, options)
return {
update(newOptions) {
instance.data = newOptions.data
instance.update()
},
destroy() {
instance.destroy()
}
}
}
</script>
<canvas use:chart={chartOptions}></canvas>
Three limits worth knowing.
-
An action must be a function declared in this component's
<script>, not imported from another file. - The parameter must not be an expression that assigns to state. It is evaluated against a snapshot, so the write goes nowhere. Pass a named function instead.
-
Inside a
<for>body, an action runs once per row with the loop variables in scope. Cleanup runs when a row leaves, and a row whose output changes is torn down and set up again rather than updated. The row scope is a snapshot, so a parameter that must track a value which changes without changing the row belongs in a component prop instead.