All skills
sveltejs avatar

/svelte-core-bestpractices

@1eb06fd official
by Sveltesveltejs/mcp331 stars
40

Guidance on writing fast, robust, modern Svelte code. Load this skill whenever in a Svelte project and asked to write/edit or analyze a Svelte component or module. Covers reactivity, event handling, styling, integration with libraries and more.

Use this Skill: https://skilld.dev/gh/sveltejs/mcp/svelte-core-bestpractices

This session only. Nothing lands on disk.

referencesawait-expressions.md

≈1.6k tokens on demand. Your agent reads this file only when SKILL.md points to it.

As of Svelte 5.36, you can use the await keyword inside your components in three places where it was previously unavailable:

  • at the top level of your component's <script>
  • inside $derived(...) declarations
  • inside your markup

This feature is currently experimental, and you must opt in by adding the experimental.async option wherever you configure Svelte, usually svelte.config.js:

/// file: svelte.config.js
export default {
	compilerOptions: {
		experimental: {
			async: true
		}
	}
};

The experimental flag will be removed in Svelte 6.

Synchronized updates

When an await expression depends on a particular piece of state, changes to that state will not be reflected in the UI until the asynchronous work has completed, so that the UI is not left in an inconsistent state. In other words, in an example like this...

<!-- codeblock:start {"title":"Synchronized updates"} -->
<!--- file: App.svelte --->
<script>
	let a = $state(1);
	let b = $state(2);

	async function add(a, b) {
		await new Promise((f) => setTimeout(f, 500)); // artificial delay
		return a + b;
	}
</script>

<input type="number" bind:value={a}>
<input type="number" bind:value={b}>

<p>{a} + {b} = {await add(a, b)}</p>
<!-- codeblock:end -->

...if you increment a, the contents of the <p> will not immediately update to read this —

<p>2 + 2 = 3</p>

— instead, the text will update to 2 + 2 = 4 when add(a, b) resolves.

Updates can overlap — a fast update will be reflected in the UI while an earlier slow update is still ongoing.

Concurrency

Svelte will do as much asynchronous work as it can in parallel. For example if you have two await expressions in your markup...

<p>{await one(x)}</p>
<p>{await two(y)}</p>

...both functions will run at the same time, as they are independent expressions, even though they are visually sequential.

This does not apply to sequential await expressions inside your <script> or inside async functions — these run like any other asynchronous JavaScript. An exception is that independent $derived expressions will update independently, even though they will run sequentially when they are first created:

// `b` will not be created until `a` has resolved,
// but once created they will update independently
// even if `x` and `y` update simultaneously
let a = $derived(await one(x));
let b = $derived(await two(y));

[!NOTE] If you write code like this, expect Svelte to give you an await_waterfall warning

Indicating loading states

To render placeholder UI, you can wrap content in a <svelte:boundary> with a pending snippet. This will be shown when the boundary is first created, but not for subsequent updates, which are globally coordinated.

After the contents of a boundary have resolved for the first time and have replaced the pending snippet, you can detect subsequent async work with $effect.pending(). This is what you would use to display a "we're asynchronously validating your input" spinner next to a form field, for example.

You can also use settled() to get a promise that resolves when the current update is complete:

import { tick, settled } from 'svelte';

async function onclick() {
	updating = true;

	// without this, the change to `updating` will be
	// grouped with the other changes, meaning it
	// won't be reflected in the UI
	await tick();

	color = 'octarine';
	answer = 42;

	await settled();

	// any updates affected by `color` or `answer`
	// have now been applied
	updating = false;
}

Error handling

Errors in await expressions will bubble to the nearest error boundary.

Server-side rendering

Svelte supports asynchronous server-side rendering (SSR) with the render(...) API. To use it, simply await the return value:

/// file: server.js
import { render } from 'svelte/server';
import App from './App.svelte';

const { head, body } = +++await+++ render(App);

[!NOTE] If you're using a framework like SvelteKit, this is done on your behalf.

If a <svelte:boundary> with a pending snippet is encountered during SSR, that snippet will be rendered while the rest of the content is ignored. All await expressions encountered outside boundaries with pending snippets will resolve and render their contents prior to await render(...) returning.

[!NOTE] In the future, we plan to add a streaming implementation that renders the content in the background.

Forking

The fork(...) API, added in 5.42, makes it possible to run await expressions that you expect to happen in the near future. This is mainly intended for frameworks like SvelteKit to implement preloading when (for example) users signal an intent to navigate.

<script>
	import { fork } from 'svelte';
	import Menu from './Menu.svelte';

	let open = $state(false);

	/** @type {import('svelte').Fork | null} */
	let pending = null;

	function preload() {
		pending ??= fork(() => {
			open = true;
		});
	}

	function discard() {
		pending?.discard();
		pending = null;
	}
</script>

<button
	onfocusin={preload}
	onfocusout={discard}
	onpointerenter={preload}
	onpointerleave={discard}
	onclick={() => {
		pending?.commit();
		pending = null;

		// in case `pending` didn't exist
		// (if it did, this is a no-op)
		open = true;
	}}
>open menu</button>

{#if open}
	<!-- any async work inside this component will start
	     as soon as the fork is created -->
	<Menu onclose={() => open = false} />
{/if}

Caveats

As an experimental feature, the details of how await is handled (and related APIs like $effect.pending()) are subject to breaking changes outside of a semver major release, though we intend to keep such changes to a bare minimum.

Breaking changes

Effects run in a slightly different order when the experimental.async option is true. Specifically, block effects like {#if ...} and {#each ...} now run before an $effect.pre or beforeUpdate in the same component, which means that in very rare situations.

Source: SKILL.md on GitHub

No alerts16d5 checks · Risk SAFE
  • Gen Agent Trust Hub16d

    The skill provides legitimate guidance and best practices for Svelte 5 development. It contains only documentation and code examples with no identified security risks.

  • Socket16d

    No alerts

  • Snyk16d

    Risk: LOW · No issues

  • Runlayer6mo

    10 files scanned · No issues

  • ZeroLeaks5mo

    1 finding · Score: 86/100

Signed by skilld at 1eb06fd. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub last week.

Activeupdated 2 months ago
  • svelte
  • reactivity
  • runes
  • components
  • styling
  • state-management
  • event-handling
  • best-practices
  • svelte5

README badge

README badge for sveltejs/mcp/svelte-core-bestpractices

Provides best practices for writing Svelte 5 components using runes (`$state`, `$derived`, `$effect`, `$props`), event handling, snippets, styling, and context. Load this skill in Svelte projects when writing or reviewing components to ensure idiomatic reactivity patterns and avoid legacy features.

Generated from the current SKILL.md.

Should I use $state for all variables?
No. Only use $state for variables that should be reactive and trigger updates in effects, derived values, or templates. Regular variables can stay as normal JavaScript. For large objects that are only reassigned (not mutated), use $state.raw instead to avoid proxy overhead.
When should I use $derived instead of $effect?
$derived should be your first choice for computing values from state. Use $effect only as an escape hatch for side effects like syncing to external libraries. Never use $effect just to update a variable based on state changes.
Can I use $effect to listen to window or document events?
No. Use <svelte:window> and <svelte:document> instead. Avoid $effect or onMount for attaching global event listeners.
What should I use instead of on:click and other legacy event directives?
Use the onclick attribute directly (e.g. <button onclick={() => {...}}>), which works with attribute shorthand and spread props.
Should I destructure items in each blocks if I need to bind to them?
No. Avoid destructuring if you need to mutate the item with something like bind:value={item.count}, as it can break reactivity.

Generated from the current SKILL.md. These answers refresh after source changes.