diff --git a/packages/svelte/src/reactivity/create-subscriber.js b/packages/svelte/src/reactivity/create-subscriber.js index 491ffb45cba7..c1ddeeea4eae 100644 --- a/packages/svelte/src/reactivity/create-subscriber.js +++ b/packages/svelte/src/reactivity/create-subscriber.js @@ -6,15 +6,78 @@ import { increment } from './utils.js'; import { DEV } from 'esm-env'; /** - * Returns a `subscribe` function that, if called in an effect (including expressions in the template), - * calls its `start` callback with an `update` function. Whenever `update` is called, the effect re-runs. + * Returns a `subscribe` function that bridges external, non-reactive changes + * to Svelte's reactivity system. It's ideal for integrating with browser APIs, + * WebSockets, or any event-based source outside of Svelte's control. * - * If `start` returns a function, it will be called when the effect is destroyed. + * Call the returned `subscribe()` function inside a getter to make that getter + * reactive. When the external source changes, you call an `update` function, + * which in turn causes any effects that depend on the getter to re-run. * - * If `subscribe` is called in multiple effects, `start` will only be called once as long as the effects - * are active, and the returned teardown function will only be called when all effects are destroyed. + * @param {(update: () => void) => (() => void) | void} start + * A callback that runs when the subscription is first activated by an effect. + * It receives an `update` function, which you should call to signal that + * the external data source has changed. The `start` callback can optionally + * return a `cleanup` function, which will be called when the last effect + * that depends on it is destroyed. + * @returns {() => void} + * A `subscribe` function that you call inside a getter to establish the + * reactive connection. + * + * @example + * ### The Generic Pattern * - * It's best understood with an example. Here's an implementation of [`MediaQuery`](https://svelte.dev/docs/svelte/svelte-reactivity#MediaQuery): + * This pattern shows how to create a reusable utility that encapsulates the + * external state and subscription logic. + * + * ```js + * import { createSubscriber } from 'svelte/reactivity'; + * + * export function createReactiveExternalState() { + * let state = someInitialValue; + * + * const subscribe = createSubscriber((update) => { + * // Set up your external listener (DOM event, WebSocket, timer, etc.) + * const cleanup = setupListener(() => { + * state = newValue; // Update your state + * update(); // Call this to trigger Svelte reactivity + * }); + * + * // Return cleanup function + * return () => cleanup(); + * }); + * + * return { + * get current() { + * subscribe(); // This "paints" the getter as reactive + * return state; + * } + * }; + * } + * ``` + * + * ### Implementation Details + * + * Internally, `createSubscriber` creates a hidden reactive `$state` variable + * that acts as a version number. Calling the `update` function increments this + * version. When the `subscribe` function is called within an effect, it reads + * this version number, creating a dependency. This mechanism ensures that + * getters become reactive to the external changes you signal. + * + * This approach is highly efficient: + * - **Lazy:** The `start` callback is only executed when the getter is first + * used inside an active effect. + * - **Automatic Cleanup:** The returned cleanup function is automatically + * called when the last subscribing effect is destroyed. + * - **Shared:** If multiple effects depend on the same getter, the `start` + * callback is still only called once. + * + * It's best understood with more examples. + * + * @example + * ### MediaQuery + * + * Here's a practical implementation of a reactive `MediaQuery` utility class. * * ```js * import { createSubscriber } from 'svelte/reactivity'; @@ -39,12 +102,58 @@ import { DEV } from 'esm-env'; * get current() { * this.#subscribe(); * - * // Return the current state of the query, whether or not we're in an effect + * // Return the current state, whether or not we're in an effect * return this.#query.matches; * } * } * ``` - * @param {(update: () => void) => (() => void) | void} start + * + * @example + * ### Mouse Position + * + * This example creates a utility that reactively tracks mouse coordinates. + * + * ```js + * import { createSubscriber } from 'svelte/reactivity'; + * import { on } from 'svelte/events'; + * + * export function createMousePosition() { + * let x = 0; + * let y = 0; + * + * const subscribe = createSubscriber((update) => { + * const handleMouseMove = (event) => { + * x = event.clientX; + * y = event.clientY; + * update(); // Trigger reactivity + * }; + * + * const off = on(window, 'mousemove', handleMouseMove); + * return () => off(); + * }); + * + * return { + * get x() { + * subscribe(); // Makes x reactive + * return x; + * }, + * get y() { + * subscribe(); // Makes y reactive + * return y; + * } + * }; + * } + * ``` + * + * ### When to use `createSubscriber` + * + * - To synchronize Svelte's reactivity with external event sources like DOM + * events, `postMessage`, or WebSockets. + * - To create reactive wrappers around browser APIs (`matchMedia`, + * `IntersectionObserver`, etc.). + * - When you have a value that is read from an external source and you need + * components to update when that value changes. It is a more direct + * alternative to using `$state` and `$effect` for this specific purpose. * @since 5.7.0 */ export function createSubscriber(start) { @@ -72,7 +181,7 @@ export function createSubscriber(start) { tick().then(() => { // Only count down after timeout, else we would reach 0 before our own render effect reruns, // but reach 1 again when the tick callback of the prior teardown runs. That would mean we - // re-subcribe unnecessarily and create a memory leak because the old subscription is never cleaned up. + // re-subscribe unnecessarily and create a memory leak because the old subscription is never cleaned up. subscribers -= 1; if (subscribers === 0) { diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 432171ae0dd1..ddd6439740b8 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -2318,15 +2318,78 @@ declare module 'svelte/reactivity' { constructor(query: string, fallback?: boolean | undefined); } /** - * Returns a `subscribe` function that, if called in an effect (including expressions in the template), - * calls its `start` callback with an `update` function. Whenever `update` is called, the effect re-runs. + * Returns a `subscribe` function that bridges external, non-reactive changes + * to Svelte's reactivity system. It's ideal for integrating with browser APIs, + * WebSockets, or any event-based source outside of Svelte's control. + * + * Call the returned `subscribe()` function inside a getter to make that getter + * reactive. When the external source changes, you call an `update` function, + * which in turn causes any effects that depend on the getter to re-run. + * + * @param start + * A callback that runs when the subscription is first activated by an effect. + * It receives an `update` function, which you should call to signal that + * the external data source has changed. The `start` callback can optionally + * return a `cleanup` function, which will be called when the last effect + * that depends on it is destroyed. + * @returns + * A `subscribe` function that you call inside a getter to establish the + * reactive connection. + * + * @example + * ### The Generic Pattern + * + * This pattern shows how to create a reusable utility that encapsulates the + * external state and subscription logic. * - * If `start` returns a function, it will be called when the effect is destroyed. + * ```js + * import { createSubscriber } from 'svelte/reactivity'; + * + * export function createReactiveExternalState() { + * let state = someInitialValue; + * + * const subscribe = createSubscriber((update) => { + * // Set up your external listener (DOM event, WebSocket, timer, etc.) + * const cleanup = setupListener(() => { + * state = newValue; // Update your state + * update(); // Call this to trigger Svelte reactivity + * }); + * + * // Return cleanup function + * return () => cleanup(); + * }); + * + * return { + * get current() { + * subscribe(); // This "paints" the getter as reactive + * return state; + * } + * }; + * } + * ``` + * + * ### Implementation Details * - * If `subscribe` is called in multiple effects, `start` will only be called once as long as the effects - * are active, and the returned teardown function will only be called when all effects are destroyed. + * Internally, `createSubscriber` creates a hidden reactive `$state` variable + * that acts as a version number. Calling the `update` function increments this + * version. When the `subscribe` function is called within an effect, it reads + * this version number, creating a dependency. This mechanism ensures that + * getters become reactive to the external changes you signal. * - * It's best understood with an example. Here's an implementation of [`MediaQuery`](https://svelte.dev/docs/svelte/svelte-reactivity#MediaQuery): + * This approach is highly efficient: + * - **Lazy:** The `start` callback is only executed when the getter is first + * used inside an active effect. + * - **Automatic Cleanup:** The returned cleanup function is automatically + * called when the last subscribing effect is destroyed. + * - **Shared:** If multiple effects depend on the same getter, the `start` + * callback is still only called once. + * + * It's best understood with more examples. + * + * @example + * ### MediaQuery + * + * Here's a practical implementation of a reactive `MediaQuery` utility class. * * ```js * import { createSubscriber } from 'svelte/reactivity'; @@ -2351,11 +2414,58 @@ declare module 'svelte/reactivity' { * get current() { * this.#subscribe(); * - * // Return the current state of the query, whether or not we're in an effect + * // Return the current state, whether or not we're in an effect * return this.#query.matches; * } * } * ``` + * + * @example + * ### Mouse Position + * + * This example creates a utility that reactively tracks mouse coordinates. + * + * ```js + * import { createSubscriber } from 'svelte/reactivity'; + * import { on } from 'svelte/events'; + * + * export function createMousePosition() { + * let x = 0; + * let y = 0; + * + * const subscribe = createSubscriber((update) => { + * const handleMouseMove = (event) => { + * x = event.clientX; + * y = event.clientY; + * update(); // Trigger reactivity + * }; + * + * const off = on(window, 'mousemove', handleMouseMove); + * return () => off(); + * }); + * + * return { + * get x() { + * subscribe(); // Makes x reactive + * return x; + * }, + * get y() { + * subscribe(); // Makes y reactive + * return y; + * } + * }; + * } + * ``` + * + * ### When to use `createSubscriber` + * + * - To synchronize Svelte's reactivity with external event sources like DOM + * events, `postMessage`, or WebSockets. + * - To create reactive wrappers around browser APIs (`matchMedia`, + * `IntersectionObserver`, etc.). + * - When you have a value that is read from an external source and you need + * components to update when that value changes. It is a more direct + * alternative to using `$state` and `$effect` for this specific purpose. * @since 5.7.0 */ export function createSubscriber(start: (update: () => void) => (() => void) | void): () => void;