eyecatch

Svelte 新機能の @attach みてみる

Posted on 2025/05/17
# Technology

svelte@5.29.0 に @attach という新しい Template Syntax が追加されました。
追加の背景と機能についてみていきたいと思います。

PR: https://github.com/sveltejs/svelte/pull/15000
Docs: https://svelte.dev/docs/svelte/@attach
Demo: Playground

// Syntax
<script lang='ts'>
	import type { Attachment } from 'svelte/attachments';
	import tippy from 'tippy.js';

	let content = $state('Hello!');

	function tooltip(content: string): Attachment<HTMLElement> {
		return (node) => {
			const tooltip = tippy(node, { content });
			return tooltip.destroy;	
		};
	}
</script>

<input bind:value={content} />

<button {@attach tooltip(content)}>
	Hover me
</button>

背景

Mount 時に Dom Element に対して処理を行いたい場合には Actions というAPIが提供されてました。

<script lang="ts">
	import type { Action } from 'svelte/action';
	import tippy from 'tippy.js';

	let content = $state('Hello!');

	function tooltip(node, fn): Action {
		$effect(() => {
			const tooltip = tippy(node, fn());

			return tooltip.destroy;
		});
	}
</script>

<input bind:value={content} />

<button use:tooltip={() => ({ content })}>
	Hover me
</button>

これが抱える問題として以下が挙げられてました。

  • <div use:foo={bar}> は一見すると foobar が等しいことを意味しているように見えますが、実際には foo(div, bar) という意味になります。文法がおかしく見た目だけではその意味を推測することは難しいです。
  • use:foo の foo は識別子でなければなりません。たとえば use:createFoo() のように関数呼び出しを直接使うことはできず、どこか別の場所で宣言されている必要があります。
  • そのため inline actions を使うことができません。
  • リアクティブではありません。もし foo が変化しても use:foo={bar} は再実行されません。 bar が変化した場合には foo が update メソッドを返していればそれは再実行されますが、そうでない場合ドキュメントが推奨しているように effects を使っても何も起こりません。
  • コンポーネントに対して使うことはできません。
  • スプレッドできないので、属性と振る舞いの両方を追加したい場合には jump through hoops が必要になります。

Actions are neat but they have a number of awkward characteristics and limitations:

  • the syntax is very weird! <div use:foo={bar}> implies some sort of equality between foo and bar but actually means foo(div, bar). There's no way you could figure that out just by looking at it
  • the foo in use:foo has to be an identifier. You can't, for example, do use:createFoo() — it must have been declared elsewhere
  • as a corollary, you can't do 'inline actions'
  • it's not reactive. If foo changes, use:foo={bar} does not re-run. If bar changes, and foo returned an update method, that method will re-run, but otherwise (including if you use effects, which is how the docs recommend you use actions) nothing will happen
  • you can't use them on components
  • you can't spread them, so if you want to add both attributes and behaviours you have to jump through hoops

We can do much better.

引用: https://github.com/sveltejs/svelte/pull/15000

@attach について

@attach を使えばより直感的かつみやすく実装できるようになりました。

<script lang='ts'>
	import type { Attachment } from 'svelte/attachments';
	import tippy from 'tippy.js';

	let content = $state('Hello!');

	function tooltip(content: string): Attachment<HTMLElement> {
		return (node) => {
			const tooltip = tippy(node, { content });
			return tooltip.destroy;	
		};
	}
</script>

<input bind:value={content} />

<button {@attach tooltip(content)}>
	Hover me
</button>

より直感的になりましたね。
<div {@attach (node) => console.log(node)}>...</div> のように Inline で定義できるようにもなりました。

下記のようにすれば Component にも使うことができます。

// Button.svelte
<script>
  let { children, ...props } = $props();
</script>

<button {...props}>{@render children?.()}</button>

// App.svelte
<Button
  class="cool-button"
  onclick={() => console.log('clicked')}
  {@attach tooltip(content)}
>
  hello
</Button>

@attach により Dom に対する処理がよりやりやすくなったと思います。
Svelteの update は開発体験を上げるものが多く、Svelte を書いてて本当に楽しいです。

Table of contents
  1. 背景
  2. @attach について