Svelteのリアクティブコアを高度なアクション、ストア、トランジションでマスターする
Olivia Novak
Dev Intern · Leapcell

はじめに
進化し続けるフロントエンド開発の状況において、パフォーマンス、開発者体験、バンドルサイズは引き続き最優先事項です。Svelteのようなフレームワークは、リアクティビティに典型的に関連付けられているランタイムオーバーヘッドの多くを「コンパイル」する独自の アプローチを提供することで、強力な候補として登場しました。Svelteのコアコンセプト(コンポーネント、リアクティビティ、バインディング)は直感的ですが、その可能性を最大限に引き出すには、高度な機能に深く踏み込むことがしばしば必要です。特に、Svelteアクション、ストア、トランジションは、基本的なアプリケーションを超えて使用されると、インタラクティビティ、保守性、洗練されたユーザーエクスペリエンスの新しいレベルを解き放つことができます。この記事では、これらのSvelteプリミティブの高度な使用法を探求し、より複雑で堅牢、かつパフォーマンスの高いWebアプリケーションにそれらを活用する方法を実証します。
Svelteの高度な機能の深掘り
高度なテクニックに入る前に、議論の基礎となるコアコンセプトを簡単に定義しましょう。
- Svelteアクション: 要素がマウントされたときに呼び出される関数であり、
update
メソッド(アクションのパラメーターが変更されたときに呼び出される)とdestroy
メソッド(要素がアンマウントされたときに呼び出される)を持つオブジェクトを返すことができます。アクションは、DOM操作やライフサイクルロジックを要素に直接カプセル化するために非常に強力です。 - Svelteストア: 状態を保持し、値が変更されたときにサブスクライバーに通知するオブジェクトです。Svelteは、シンプルな書き込み可能、読み取り可能、導出ストアを提供し、リアクティブな状態管理のバックボーンを形成します。
- Svelteトランジション: 要素がDOMに追加または削除されたときに適用されるアニメーションです。Svelteは、組み込みトランジションのセットとカスタムトランジションを作成するための柔軟なAPIを提供し、スムーズで魅力的なUI変更を保証します。
高度なSvelteアクション:要素の動作をオーケストレーションする
基本的なアクションはツールチップやドラッグ&ドロップのような単純なタスクには適していますが、その真の力は、複雑な要素の動作をオーケストレーションし、外部ライブラリと統合することにあります。
原則:DOM中心のロジックのカプセル化
高度なアクションの基本アイデアは、すべてのDOM操作またはライフサイクル依存のロジックを再利用可能なアクションにカプセル化することです。これにより、コンポーネントスクリプトはクリーンに保たれ、データフローに焦点を当て、アクションがDOMとの対話の複雑な詳細を処理します。
例: "Click Outside"リスナーの実装
一般的なUIパターンは、ユーザーがドロップダウンやモーダルを外部でクリックしたときにそれを閉じることです。これは、Svelteアクションでエレガントに実装できます。
<!-- ClickOutside.svelte --> <script> import { createEventDispatcher } from 'svelte'; import { tick } from 'svelte'; const dispatch = createEventDispatcher(); export function clickOutside(node) { const handleClick = (event) => { if (node && !node.contains(event.target) && !event.defaultPrevented) { dispatch('clickoutside'); } }; // tickを使用し、コンポーネントがレンダリングされた後にイベントリスナーが追加されることを保証します。 // コンポーネントがユーザーアクションの直後に直接マウントされた場合に、即時トリガーを防ぐため。 tick().then(() => { document.addEventListener('click', handleClick, true); // true はキャプチャフェーズ用 }); return { destroy() { document.removeEventListener('click', handleClick, true); } }; } </script> <div use:clickOutside on:clickoutside={() => alert('Clicked outside!')}> Click inside me! </div>
アプリケーションシナリオ: このアクションは、カスタムドロップダウン、ナビゲール可能なメニュー、モーダル、またはその境界外のインタラクションに反応する必要があるUI要素を構築するのに非常に役立ち、モジュール性と再利用性を促進します。
例:設定可能な動的ツールチップ
アクションはパラメーターを受け取り、動的な設定を可能にすることもできます。コンテンツと配置で設定可能なツールチップアクションを作成しましょう。
<!-- Tooltip.svelte --> <script> // .tooltip および .tooltip-content の CSS フレームワークまたはカスタムスタイルを想定 export function tooltip(node, params) { let tooltipEl; function updateTooltip(newParams) { const { content, placement = 'top' } = newParams; if (!content) return; if (!tooltipEl) { tooltipEl = document.createElement('div'); tooltipEl.className = `tooltip tooltip-${placement}`; document.body.appendChild(tooltipEl); node.addEventListener('mouseenter', showTooltip); node.addEventListener('mouseleave', hideTooltip); node.addEventListener('focus', showTooltip); node.addEventListener('blur', hideTooltip); } tooltipEl.innerHTML = content; positionTooltip(tooltipEl, node, placement); } function showTooltip() { if (tooltipEl) tooltipEl.style.display = 'block'; } function hideTooltip() { if (tooltipEl) tooltipEl.style.display = 'none'; } function positionTooltip(tip, target, placement) { const targetRect = target.getBoundingClientRect(); const tipRect = tip.getBoundingClientRect(); let top, left; switch (placement) { case 'top': top = targetRect.top - tipRect.height - 5; left = targetRect.left + (targetRect.width / 2) - (tipRect.width / 2); break; case 'bottom': top = targetRect.bottom + 5; left = targetRect.left + (targetRect.width / 2) - (tipRect.width / 2); break; // ... さらに多くの配置を追加 default: top = targetRect.top - tipRect.height - 5; left = targetRect.left + (targetRect.width / 2) - (tipRect.width / 2); } tip.style.top = `${top + window.scrollY}px`; tip.style.left = `${left + window.scrollX}px`; } updateTooltip(params); // 初期設定 return { update(newParams) { updateTooltip(newParams); }, destroy() { if (tooltipEl) { document.body.removeChild(tooltipEl); node.removeEventListener('mouseenter', showTooltip); node.removeEventListener('mouseleave', hideTooltip); node.removeEventListener('focus', showTooltip); node.removeEventListener('blur', hideTooltip); } } }; } </script> <button use:tooltip={{ content: 'This is a Svelte tooltip!', placement: 'bottom' }}>Hover me</button>
アプリケーションシナリオ: この動的なツールチップアクションは、さまざまなコンポーネント全体で使用でき、コンポーネントロジックを散らかすことなく、コンテキスト情報を提供するための、一貫性があり設定可能な方法を提供します。
高度なSvelteストア:単純な状態を超えて
Svelteストアは、単純なグローバル状態のために導入されることがよくあります。しかし、それらの derived
およびカスタムストア機能は、特に非同期操作や異なる状態間の関係を扱う場合、複雑なリアクティブデータ管理のための強力なパターンを提供します。
原則:導出された状態とカスタムストアロジック
高度なストアの使用は、既存のストアから新しいリアクティブストアを作成すること (derived
)、およびカスタムストア内に非同期操作を含む複雑なロジックをカプセル化することに焦点を当てています。これにより、コンポーネントロジックは宣言的であり続け、ストア値の変更のみに反応します。
例: "Fetch Status"ストア
データを取得しており、データだけでなく、ローディング状態と潜在的なエラーも追跡したいシナリオを想像してください。カスタムストアは、このライフサイクル全体をカプセル化できます。
// stores/fetchStatus.js import { writable, get } from 'svelte/store'; export function createFetchStatusStore(initialData = null) { const { subscribe, set, update } = writable({ data: initialData, loading: false, error: null, }); async function fetchData(url, options = {}) { update(s => ({ ...s, loading: true, error: null })); try { const response = await fetch(url, options); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); set({ data, loading: false, error: null }); return data; // 外部からのデータアクセスを許可 } catch (error) { set({ data: initialData, loading: false, error: error.message }); throw error; // エラーを伝播させる } } function reset() { set({ data: initialData, loading: false, error: null }); } return { subscribe, fetchData, reset, get value() { // 利便性のためのゲッター return get({ subscribe }); } }; }
<!-- コンポーネントでの使用 --> <script> import { createFetchStatusStore } from './stores/fetchStatus.js'; const userStore = createFetchStatusStore(); let userId = 1; async function loadUser() { try { await userStore.fetchData(`https://jsonplaceholder.typicode.com/users/${userId}`); } catch (e) { console.error('Failed to load user:', e); } } $: if (userId) { // userId 変更時のリアクティブなデータ取得 loadUser(); } </script> <div> <h2>User Details</h2> {#if $userStore.loading} <p>Loading user...</p> {:else if $userStore.error} <p class="error">Error: {$userStore.error}</p> {:else if $userStore.data} <p>Name: {$userStore.data.name}</p> <p>Email: {$userStore.data.email}</p> {/if} <input type="number" bind:value={userId} min="1" max="10" /> </div> <style> .error { color: red; } </style>
アプリケーションシナリオ: このパターンは、非同期データ取得、フォーム送信、またはユーザーに明確なフィードバック(ローディング、成功、エラー)を表示する必要がある長時間実行プロセスを管理するのに理想的です。ロジックを一元化し、コンポーネントをよりクリーンでレンダリングに集中させます。
例:複雑なフィルタリングのための導出ストア
複数の基準に基づいてフィルタリングとソートが必要なアイテムのリストがあるとします。導出ストアは、リアクティブで効率的なソリューションを提供できます。
// stores/itemStore.js import { writable, derived } from 'svelte/store'; const items = writable([ { id: 1, name: 'Apple', category: 'Fruit', price: 1.0 }, { id: 2, name: 'Carrot', category: 'Vegetable', price: 0.5 }, { id: 3, name: 'Banana', category: 'Fruit', price: 1.2 }, { id: 4, name: 'Broccoli', category: 'Vegetable', price: 0.8 }, { id: 5, name: 'Orange', category: 'Fruit', price: 1.1 }, ]); export const searchTerm = writable(''); export const selectedCategory = writable('All'); export const sortBy = writable('name'); // 'name', 'price' export const filteredAndSortedItems = derived( [items, searchTerm, selectedCategory, sortBy], ([$items, $searchTerm, $selectedCategory, $sortBy]) => { let filtered = $items.filter(item => item.name.toLowerCase().includes($searchTerm.toLowerCase()) && ($selectedCategory === 'All' || item.category === $selectedCategory) ); filtered.sort((a, b) => { if ($sortBy === 'name') { return a.name.localeCompare(b.name); } else if ($sortBy === 'price') { return a.price - b.price; } return 0; }); return filtered; } );
<!-- コンポーネントでの使用 --> <script> import { searchTerm, selectedCategory, sortBy, filteredAndSortedItems } from './stores/itemStore.js'; const categories = ['All', 'Fruit', 'Vegetable']; </script> <div> <input type="text" placeholder="Search items..." bind:value={$searchTerm} /> <select bind:value={$selectedCategory}> {#each categories as category} <option value={category}>{category}</option> {/each} </select> <select bind:value={$sortBy}> <option value="name">Sort by Name</option> <option value="price">Sort by Price</option> </select> <ul> {#each $filteredAndSortedItems as item (item.id)} <li>{item.name} - {item.category} - ${item.price.toFixed(2)}</li> {/each} </ul> </div>
アプリケーションシナリオ: これは、ダッシュボード、製品リスト、またはフィルタリング、ソート、またはページネーションを介してデータをインタラクティブに探索する必要があるアプリケーションに最適です。これにより、あらゆるフィルターの変更が、明示的な再レンダリング呼び出しなしに表示データを即座に更新する、非常にリアクティブなUIが促進されます。
高度なSvelteトランジション:シームレスなユーザーエクスペリエンスの作成
Svelteの組み込みトランジション(fade
、slide
、scale
、fly
、blur
、draw
)は優れていますが、カスタムトランジションとトランジショングループは、非常に特定の調整されたアニメーションを可能にし、真に洗練されたユーザーインターフェイスを作成します。
原則:中断動作と調整されたアニメーションのカスタマイズ
高度なトランジションは、カスタムイージング関数、そのライフサイクルに対する正確な制御、および複数のトランジション効果の調整をしばしば含みます。重要な側面は、トランジションが急速にトリガーされたときの動作を管理し、ぎこちないジャンプを防ぐことです。
例: "Squishy"カスタムトランジション
バネのような動きを使用するカスタム squish
トランジションを作成しましょう。
<!-- コンポーネントでの使用 --> <script> import { elasticOut } from 'svelte/easing'; let show = false; function squish(node, { duration = 400 }) { return { duration, easing: elasticOut, css: (t, u) => ` transform: scaleY(${t}) scaleX(${1 + u * 0.1}); opacity: ${t}; ` }; } </script> <style> .squishy { background-color: lightblue; padding: 20px; border-radius: 8px; display: inline-block; margin-top: 20px; } </style> <button on:click={() => (show = !show)}>Toggle Squishy Box</button> {#if show} <div class="squishy" transition:squish> Hello, Squishy World! </div> {/if}
アプリケーションシナリオ: squish
のようなカスタムトランジションは、アプリケーションに独自のブランド感覚を追加するために使用でき、要素を独特で記憶に残る方法でアニメートさせることができます。確認、通知、または目立たせる必要がある重要なUI要素に特に効果的です。
例:調整されたグループトランジション
複数の要素が同時に追加または削除される場合、特定のシーケンスでアニメートしたり、遅延効果を適用したりしたい場合があります。Svelteの crossfade
は特定のユースケース向けですが、調整されたトランジションの力についてのヒントを与えます。より一般的なアプローチは、カスタムトランジションを #each
ブロックと遅延を組み合わせて使用することです。
<!-- CoordinatedList.svelte --> <script> import { fly } from 'svelte/transition'; import { cubicOut } from 'svelte/easing'; let items = ['Item 1', 'Item 2', 'Item 3']; let counter = items.length + 1; function addItem() { items = [...items, `Item ${counter}`]; counter++; } function removeItem(itemToRemove) { items = items.filter(item => item !== itemToRemove); } // インデックスに基づいて遅延を生成するヘルパー関数 function delayedFly(node, { delayIn = 0, delayOut = 0, duration = 400, ...rest }) { return { ...fly(node, { delay: delayIn, duration, easing: cubicOut, y: -10, // 少し上に飛ぶ ...rest }), out: fly(node, { delay: delayOut, duration, easing: cubicOut, y: 10, // 少し下に飛ぶ ...rest }) }; } </script> <style> ul { list-style: none; padding: 0; } li { background-color: #f0f0f0; margin-bottom: 5px; padding: 10px; border-radius: 4px; display: flex; justify-content: space-between; align-items: center; } button { margin-left: 10px; } </style> <button on:click={addItem}>Add Item</button> <ul> {#each items as item, i (item)} <li transition:delayedFly={{ delayIn: i * 50, delayOut: 0 }}> {item} <button on:click={() => removeItem(item)}>Remove</button> </li> {/each} </ul>
アプリケーションシナリオ: この段差アニメーショントラフィックは、検索結果、通知スタック、またはアイテムが動的に追加または削除されるあらゆるシナリオを表示するのに最適で、視覚的な手がかりとより流動的なユーザーエクスペリエンスを提供します。delayIn
パラメーターは、入力要素の「ウォーターフォール」効果を作成します。
結論
Svelteアクション、ストア、トランジションは、基盤となる概念以上のものです。それらは、創造的に理解され応用されると、開発者が非常にインタラクティブで、パフォーマンスが高く、保守性の高い Web アプリケーションを構築できるようにする強力なプリミティブです。アクションに DOM ロジックをカプセル化し、カスタムストアと導出ストアで複雑な状態を管理し、高度なトランジションでシームレスなユーザー エクスペリエンスを作成することにより、プロジェクトをプロフェッショナルな基準に引き上げることができます。これらの高度なテクニックを習得することは、ボイラープレートを最小限に抑え、柔軟性を最大化する、宣言的で効率的なフロントエンド開発へのアプローチを可能にします。
最終的には、これらの高度なパターンを通じて Svelte のリアクティブ コアを活用することで、パフォーマンスが高いだけでなく、構築や使用が楽しいアプリケーションを開発できます。