diff --git a/.changeset/two-dancers-speak.md b/.changeset/two-dancers-speak.md new file mode 100644 index 0000000000..b0d8d394d0 --- /dev/null +++ b/.changeset/two-dancers-speak.md @@ -0,0 +1,5 @@ +--- +'svelte': minor +--- + +feat: add `$effect.allowed` rune diff --git a/documentation/docs/02-runes/04-$effect.md b/documentation/docs/02-runes/04-$effect.md index d41c5b8e6a..5afb9fb783 100644 --- a/documentation/docs/02-runes/04-$effect.md +++ b/documentation/docs/02-runes/04-$effect.md @@ -204,24 +204,6 @@ In rare cases, you may need to run code _before_ the DOM updates. For this we ca Apart from the timing, `$effect.pre` works exactly like `$effect`. -## `$effect.tracking` - -The `$effect.tracking` rune is an advanced feature that tells you whether or not the code is running inside a tracking context, such as an effect or inside your template ([demo](/playground/untitled#H4sIAAAAAAAACn3PwYrCMBDG8VeZDYIt2PYeY8Dn2HrIhqkU08nQjItS-u6buAt7UDzmz8ePyaKGMWBS-nNRcmdU-hHUTpGbyuvI3KZvDFLal0v4qvtIgiSZUSb5eWSxPfWSc4oB2xDP1XYk8HHiSHkICeXKeruDDQ4Demlldv4y0rmq6z10HQwuJMxGVv4mVVXDwcJS0jP9u3knynwtoKz1vifT_Z9Jhm0WBCcOTlDD8kyspmML5qNpHg40jc3fFryJ0iWsp_UHgz3180oBAAA=)): - -```svelte - - -
in template: {$effect.tracking()}
-``` - -It is used to implement abstractions like [`createSubscriber`](/docs/svelte/svelte-reactivity#createSubscriber), which will create listeners to update reactive values but _only_ if those values are being tracked (rather than, for example, read inside an event handler). - ## `$effect.pending` When using [`await`](await-expressions) in components, the `$effect.pending()` rune tells you how many promises are pending in the current [boundary](svelte-boundary), not including child boundaries ([demo](/playground/untitled#H4sIAAAAAAAAE3WRMU_DMBCF_8rJdHDUqilILGkaiY2RgY0yOPYZWbiOFV8IleX_jpMUEAIWS_7u-d27c2ROnJBV7B6t7WDsequAozKEqmAbpo3FwKqnyOjsJ90EMr-8uvN-G97Q0sRaEfAvLjtH6CjbsDrI3nhqju5IFgkEHGAVSBDy62L_SdtvejPTzEU4Owl6cJJM50AoxcUG2gLiVM31URgChyM89N3JBORcF3BoICA9mhN2A3G9gdvdrij2UJYgejLaSCMsKLTivNj0SEOf7WEN7ZwnHV1dfqd2dTsQ5QCdk9bI10PkcxexXqcmH3W51Jt_le2kbH8os9Y3UaTcNLYpDx-Xab6GTHXpZ128MhpWqDVK2np0yrgXXqQpaLa4APDLBkIF8bd2sYql0Sn_DeE7sYr6AdNzvgljR-MUq7SwAdMHeUtgHR4CAAA=)): @@ -256,6 +238,54 @@ const destroy = $effect.root(() => { destroy(); ``` +Effect roots are also created when you [`mount`](imperative-component-api#mount) or [`hydrate`](imperative-component-api#hydrate) a component. + +## `$effect.tracking` + +The `$effect.tracking` rune is an advanced feature that tells you whether or not the code is running inside a tracking context, such as an effect or inside your template ([demo](/playground/untitled#H4sIAAAAAAAACn3PwYrCMBDG8VeZDYIt2PYeY8Dn2HrIhqkU08nQjItS-u6buAt7UDzmz8ePyaKGMWBS-nNRcmdU-hHUTpGbyuvI3KZvDFLal0v4qvtIgiSZUSb5eWSxPfWSc4oB2xDP1XYk8HHiSHkICeXKeruDDQ4Demlldv4y0rmq6z10HQwuJMxGVv4mVVXDwcJS0jP9u3knynwtoKz1vifT_Z9Jhm0WBCcOTlDD8kyspmML5qNpHg40jc3fFryJ0iWsp_UHgz3180oBAAA=)): + +```svelte + + +in template: {$effect.tracking()}
+``` + +It is used to implement abstractions like [`createSubscriber`](/docs/svelte/svelte-reactivity#createSubscriber), which will create listeners to update reactive values but _only_ if those values are being tracked (rather than, for example, read inside an event handler). + +## `$effect.allowed` + +The `$effect.allowed` rune is an advanced feature that indicates whether or not an effect (or [async `$derived`](await-expressions)) can be created — in other words, that we are inside the context of an effect root. + +```svelte + + + +``` + +The difference between `$effect.allowed()` and `$effect.tracking()` is that `$effect.tracking()` will return `false` at the top level of a component (or directly inside an effect root), since reading state at that moment will _not_ cause anything to re-run when the state changes. + ## When not to use `$effect` In general, `$effect` is best considered something of an escape hatch — useful for things like analytics and direct DOM manipulation — rather than a tool you should use frequently. In particular, avoid using it to synchronise state. Instead of this... diff --git a/packages/svelte/src/ambient.d.ts b/packages/svelte/src/ambient.d.ts index 159a568477..0501cfc197 100644 --- a/packages/svelte/src/ambient.d.ts +++ b/packages/svelte/src/ambient.d.ts @@ -257,6 +257,36 @@ declare namespace $derived { declare function $effect(fn: () => void | (() => void)): void; declare namespace $effect { + /** + * The `$effect.allowed` rune is an advanced feature that indicates whether an effect or async `$derived` can be created in the current context. + * Effects and async deriveds can only be created in root effects, which are created during component setup, or can be programmatically created via `$effect.root`. + * + * Example: + * ```svelte + * + * + * + * ``` + * + * @see {@link https://svelte.dev/docs/svelte/$effect#$effect.allowed Documentation} + */ + export function allowed(): boolean; /** * Runs code right before a component is mounted to the DOM, and then whenever its dependencies change, i.e. `$state` or `$derived` values. * The timing of the execution is right before the DOM is updated. diff --git a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js index 52eba8c735..41a552b567 100644 --- a/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/2-analyze/visitors/CallExpression.js @@ -150,6 +150,7 @@ export function CallExpression(node, context) { break; + case '$effect.allowed': case '$effect.tracking': if (node.arguments.length !== 0) { e.rune_invalid_arguments(node, rune); diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js index f69bc5fe6e..49722ce883 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/CallExpression.js @@ -17,6 +17,9 @@ export function CallExpression(node, context) { case '$host': return b.id('$$props.$$host'); + case '$effect.allowed': + return b.call('$.effect_allowed'); + case '$effect.tracking': return b.call('$.effect_tracking'); diff --git a/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js b/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js index 8525fb6366..45170aa71c 100644 --- a/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js +++ b/packages/svelte/src/compiler/phases/3-transform/server/visitors/CallExpression.js @@ -23,7 +23,7 @@ export function CallExpression(node, context) { return b.void0; } - if (rune === '$effect.tracking') { + if (rune === '$effect.tracking' || rune === '$effect.allowed') { return b.false; } diff --git a/packages/svelte/src/internal/client/index.js b/packages/svelte/src/internal/client/index.js index af8df2e32c..7fd37af234 100644 --- a/packages/svelte/src/internal/client/index.js +++ b/packages/svelte/src/internal/client/index.js @@ -114,6 +114,7 @@ export { } from './reactivity/deriveds.js'; export { aborted, + effect_allowed, effect_tracking, effect_root, legacy_pre_effect, diff --git a/packages/svelte/src/internal/client/reactivity/effects.js b/packages/svelte/src/internal/client/reactivity/effects.js index 157587e218..a84f873406 100644 --- a/packages/svelte/src/internal/client/reactivity/effects.js +++ b/packages/svelte/src/internal/client/reactivity/effects.js @@ -179,6 +179,14 @@ export function effect_tracking() { return active_reaction !== null && !untracking; } +/** + * Internal representation of `$effect.allowed()` + * @returns {boolean} + */ +export function effect_allowed() { + return active_effect !== null && !is_destroying_effect; +} + /** * @param {() => void} fn */ diff --git a/packages/svelte/src/utils.js b/packages/svelte/src/utils.js index 57561e6dc7..5859d5c401 100644 --- a/packages/svelte/src/utils.js +++ b/packages/svelte/src/utils.js @@ -442,6 +442,7 @@ const RUNES = /** @type {const} */ ([ '$props.id', '$bindable', '$effect', + '$effect.allowed', '$effect.pre', '$effect.tracking', '$effect.root', diff --git a/packages/svelte/tests/signals/test.ts b/packages/svelte/tests/signals/test.ts index 5486ccdb45..9c3c883f95 100644 --- a/packages/svelte/tests/signals/test.ts +++ b/packages/svelte/tests/signals/test.ts @@ -4,6 +4,7 @@ import * as $ from '../../src/internal/client/runtime'; import { push, pop } from '../../src/internal/client/context'; import { effect, + effect_allowed, effect_root, render_effect, user_effect, @@ -1391,6 +1392,43 @@ describe('signals', () => { }; }); + test('$effect.allowed()', () => { + const log: Array