SuperAPI AI Directory

svelte-lifecycle

Frontend & websites

View repository

Updated 10-16 10:56

About this skill

Svelte Lifecycle, Stores, Context, Testing (Svelte 5)

This skill covers subsystems in Svelte 5 that are "non-Runes" but still part of the core reactive architecture: lifecycle hooks (onMount / onDestroy / tick)、Stores(writable / readable / derived / custom), Context API (createContext / setContext / getContext), and the testing system (Vitest unit and component testing, Storybook, Playwright e2e). It also covers the browser support matrix and exceptions.

When to use this skill

  • Need to access the DOM after the component mounts (onMount) or clean up before the component is destroyed (onDestroy)
  • Need to wait for DOM updates after a state change (tick)
  • Need to share reactive state across components, but do not want to use a import module (use Stores or Context)
  • Need to write cross-component asynchronous flows (Readable/derived store)
  • Need to use Context to replace prop drilling, and when SSR is involved, do not pollute the global scope
  • Need to write Vitest unit/component tests, Storybook stories, or Playwright e2e for Svelte components
  • Need to check minimum browser versions and exceptional features

Critical: onMount

onMount(fn) Runs immediately after the component is mounted to the DOM.Runs only in the browser—not called during SSR.

Basic usage

<script>
  import { onMount } from 'svelte';

  onMount(() => {
    console.log('mounted');
  });
</script>

Return a cleanup function

<script>
  import { onMount } from 'svelte';

  onMount(() => {
    const interval = setInterval(() => console.log('beep'), 1000);
    return () => clearInterval(interval);
  });
</script>

Key constraints:onMount must receive asynchronous functionto correctly return cleanup.async () => {} always returns Promise; cleanup will not be called on unmount.

Calling from an external module

onMount does not have to be written at the top level of a component script—it must only be called during component initialization. Calling it from a helper function in the same module is allowed.

Critical: onDestroy

onDestroy(fn) Before the component is destroyed,immediatelyexecutes.It also runs in SSR components—this is the only one of the four lifecycle hooks that executes on the server side.

<script>
  import { onDestroy } from 'svelte';

  onDestroy(() => {
    console.log('destroyed');
  });
</script>

Common uses: clean up setInterval, cancel fetch AbortController, unbind global event listeners.

Critical: tick

tick() returns Promise, resolves after all pending state changes have been applied to the DOM; if there are no pending changes, it resolves in the next microtask.

<script>
  import { tick } from 'svelte';

  async function handle() {
    count = count + 1;
    await tick();
    // 此时 DOM 已更新
    element.scrollIntoView();
  }
</script>

Common scenarios: focus, scroll, measure DOM, call third-party imperative APIs after a state change.

Deprecated: beforeUpdate / afterUpdate

The "before and after the whole component updates" hook from the Svelte 4 era. In Svelte 5,it is shimmed but unavailable in runes components, so use instead:

Svelte 4 Svelte 5
beforeUpdate(() => {}) $effect.pre(() => {})
afterUpdate(() => {}) $effect(() => {})

The Runes version triggers only when explicitly read state changes, and is more precise. For example, a chat window only scrolls to the bottom when messages changes, and theme switching will not reset the scroll position.

Critical: Stores (writable, readable, derived)

A Store is an object that satisfies the "store contract": it must have subscribe(fn) → unsubscribe; a writable store must also have set(value)。

<script>
  import { writable } from 'svelte/store';

  const count = writable(0);
  $count;        // 自动订阅,读取当前值
  count.set(1);  // 写入
  $count = 2;    // 语法糖:等价于 count.set(2)
</script>

writable

import { writable } from 'svelte/store';

const count = writable(0, () => {
  // 第一个订阅者订阅时调用
  return () => {
    // 最后一个订阅者退订时调用
  };
});

The second argument is the start/stop function, commonly used to establish external connections (WebSocket, timers).

readable

A store that cannot be set externally; initial value + start function:

import { readable } from 'svelte/store';

const time = readable(new Date(), (set) => {
  set(new Date());
  const id = setInterval(() => set(new Date()), 1000);
  return () => clearInterval(id);
});

derived

Derive from one or more stores:

import { derived, writable } from 'svelte/store';

const a = writable(1);
const b = writable(2);
const sum = derived([a, b], ([$a, $b]) => $a + $b);

Asynchronous version: accepts (values, set, update), allows calling set/updatein the async callback; can return a cleanup function; can pass a third argument as the initial value.

readonly / get

  • readonly(store): wraps as a read-only view (without set/update)
  • get(store): reads once synchronously (internally subscribes → reads → unsubscribes,Not recommended for hot paths)

When to use stores vs $state

Svelte 5 recommends prioritizing runes ($state、.svelte.js module):

Scenario Recommendation
Extracting reusable logic .svelte.js file + $state
Cross-component shared state Module-level $state object
Complex asynchronous data flow Stores(readable + timers/subscriptions)
Interop with RxJS Stores($ auto-subscription)
Simple cross-component counter Either works

Simple rule: if you can use runes, use runes; use stores for "event streams/observable sequences".

Critical: Context API

Context lets a parent component pass values to descendant components at any depth without prop drilling.

createContext (recommended, Svelte 5.40+)

// context.ts
import { createContext } from 'svelte';
interface User { name: string; }
export const [getUser, setUser] = createContext<User>();
<!-- Parent.svelte -->
<script>
  import { setUser } from './context';
  setUser({ name: 'world' });
</script>
<!-- Child.svelte -->
<script>
  import { getUser } from './context';
  const user = getUser();
</script>

<h1>hello {user.name}</h1>

setContext / getContext (alternative)

<!-- Parent -->
<script>
  import { setContext } from 'svelte';
  setContext('my-key', value);
</script>

<!-- Child -->
<script>
  import { getContext } from 'svelte';
  const value = getContext('my-key');
</script>

Keys and values can be any JS value.createContext provides type safety and requires no key.

hasContext / getAllContexts

Check whether a certain key exists in the current component context hierarchy;getAllContexts() returns a Map of all current contexts. Often used in library internals.

Combining with state

Set the $state object into context,do not reassign—otherwise the reactive link is broken:

<!-- 错误 -->
<button onclick={() => counter = { count: 0 } }>reset</button>

<!-- 正确:原地修改 -->
<button onclick={() => counter.count = 0}>reset</button>

Svelte will emit a warning.

Replacing global state (critical for SSR)

Module-level $state is shared across requests under SSR (data leakage). Context is request-isolated, soprefer Context when user-specific data is involved。

Critical: Testing

Vitest unit testing

// counter.svelte.test.js
import { flushSync } from 'svelte';
import { expect, test } from 'vitest';

test('Counter', () => {
  let count = $state(0);
  count = 1;
  flushSync();
  expect(count).toBe(1);
});

Component testing

vite.config.js:

import { defineConfig } from 'vitest/config';
export default defineConfig({
  test: { environment: 'jsdom' },
  resolve: process.env.VITEST ? { conditions: ['browser'] } : undefined
});
import { mount, unmount, flushSync } from 'svelte';
import { expect, test } from 'vitest';
import Component from './Component.svelte';

test('Component', () => {
  const c = mount(Component, { target: document.body, props: { n: 0 } });
  expect(document.body.innerHTML).toBe('<button>0</button>');
  document.body.querySelector('button').click();
  flushSync();
  expect(document.body.innerHTML).toBe('<button>1</button>');
  unmount(c);
});

$effect does not run automatically during mount ; use flushSync() in tests to force synchronous triggering.

Testing with $effect

import { flushSync } from 'svelte';
import { expect, test } from 'vitest';

test('Effect', () => {
  const cleanup = $effect.root(() => {
    let count = $state(0);
    let log = [];
    $effect(() => log.push(count));
    flushSync();
    expect(log).toEqual([0]);
    count = 1;
    flushSync();
    expect(log).toEqual([0, 1]);
  });
  cleanup();
});

Storybook

Configured via npx sv add storybook ; use the play function to simulate user interactions and assert:

<Story name="Filled" play={async ({ canvas, userEvent }) => {
  await userEvent.type(canvas.getByTestId('email'), 'a@b.com');
  await userEvent.click(canvas.getByRole('button'));
  await expect(canvas.getByText("You're in!")).toBeInTheDocument();
}} />

Playwright e2e

// tests/home.spec.js
import { expect, test } from '@playwright/test';
test('h1 visible', async ({ page }) => {
  await page.goto('/');
  await expect(page.locator('h1')).toBeVisible();
});

playwright.config.js configure webServer to start the preview server.

Critical: Browser support

Baseline target 2020 (Baseline 2020):

Browser Minimum version
Chrome/Edge 87
Firefox 83
Safari 14
Opera 73
Opera (Android) 62
Samsung Internet 14.0
Android WebView 87
Internet Explorer Not supported

Exceptional features (require a higher version)

Feature Chrome/Edge Firefox Safari
$state.snapshot 98 94 15.4
bind:devicePixelContentBoxSize — 93 Not supported
flip from svelte/animate — 126 —

Quick Fixes

  • onMount cleanup does not execute → the function is async; use a synchronous function and internally await。
  • store reference in UI does not update → must use the $store prefix to read; do not destructure (const { subscribe } = store does not auto-subscribe).
  • derived does not trigger → no store value is read in the callback; or the array form derived([a,b], ([$a,$b]) => ...)。
  • Context leaks between SSR → module-level $state is globally shared; use setContext。
  • In Vitest document is not defined → test.environment: 'jsdom'; or add at the top of the file // @vitest-environment jsdom。
  • State does not take effect after component mount → call flushSync()。
  • Playwright cannot find the element → check whether webServer.command started correctly; increase timeout。

Gotchas

  • store must be declared at the top level of the component: it cannot be inside an if block or function.
  • Do not add the $ prefix to non-store variables— will be recognized as store.
  • onMount is the only "after mount", there is no "after render"; use tick() to wait for the DOM.
  • onMount does not run during SSR—accessing window / document must be placed in onMount or onDestroy (onDestroy runs during SSR).
  • setContext/getContext must be called synchronously, during the setup phase (cannot be inside an event handler).
  • If the Context value is a primitive value (number/string), changes do not trigger updates—use $state an object or a function getter.
  • $state.snapshot Browsers require 98+ / Safari 15.4+—check when releasing for compatibility.
  • Order of cleanups bound to onMount: when onMount unmounts, cleanup is called first, then onDestroy.

FAQ

Q: Does Svelte 5 still need stores? A: For simple state use the $state + .svelte.js module; stores still have value for complex async flows, timers, external subscriptions, and RxJS interoperability.

Q: onMount vs $effect, how to choose? A: Use onMount when you need to access DOM nodes or start side effects (fetch, timers, binding window events); use $effect。

Q: Why doesn't the cleanup of an async function in onMount execute? A: async () => {} always returns Promise;onMount skips cleanup when it receives a non-function. Switch to wrapping the async logic in a synchronous function:

onMount(() => {
  let cancelled = false;
  (async () => {
    const data = await fetch(...);
    if (!cancelled) state = data;
  })();
  return () => { cancelled = true; };
});

Q: When should Context replace props? A: When a value needs to pass through 3+ layers of intermediate components, or when the parent component does not directly know the child component (such as {@render children()}).

Q: What is the difference between Vitest unit tests and component tests? A: Unit tests pure logic (runes in .svelte.js files), no DOM; component tests use jsdom to render the full Svelte component.

Q: Can Svelte 5 still be used on IE 11? A: Not supported. Minimum Chrome 87 / Firefox 83 / Safari 14.

Q: Is it safe to use $state.snapshot in SSR projects? A: The server side is Node, with no browser version requirements; only check the exception table when using it on the client.

Q: Do I need to build before running tests with Playwright? A: playwright.config.js with webServer: { command: 'npm run build && npm run preview', port: 4173 }configured, Playwright automatically starts the preview server.

Examples

File Content
examples/lifecycle-hooks.md onMount cleanup, onDestroy, tick DOM measurement, deprecated hooks
examples/stores-advanced.md writable/readable/derived/custom store, async flows
examples/store-patterns.md stores vs $state, persistence, cross-component patterns
examples/context-advanced.md createContext, setContext, composing with state, SSR safety
examples/testing-vitest.md unit/component tests, $effect.root, flushSync, context wrapper
examples/testing-playwright.md e2e tests, Playwright config

References

File Content
references/lifecycle-hooks-reference.md onMount/onDestroy/tick full signatures, SSR considerations
references/stores-api-reference.md all store APIs, TypeScript types, contracts
references/context-api-reference.md createContext/setContext/getContext/hasContext, SSR behavior
references/testing-reference.md full Vitest/Storybook/Playwright setup
references/browser-support.md browser matrix, exception features, polyfill guide