feat: add $effect.root rune (#9638)

* feat: effect-root-rune

feat: add $effect.root rune

update doc

update doc

fix validation

* cleanup logic

* Update sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md

* address feedback

---------

Co-authored-by: Rich Harris <richard.a.harris@gmail.com>
pull/9650/head
Dominic Gannaway 7 months ago committed by GitHub
parent 2660727a93
commit 81d3e47d1c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: add $effect.root rune

@ -519,6 +519,12 @@ function validate_call_expression(node, scope, path) {
error(node, 'invalid-rune-args-length', '$effect.active', [0]);
}
}
if (rune === '$effect.root') {
if (node.arguments.length !== 1) {
error(node, 'invalid-rune-args-length', '$effect.root', [1]);
}
}
}
/**

@ -135,7 +135,7 @@ export const javascript_visitors_runes = {
for (const declarator of node.declarations) {
const init = declarator.init;
const rune = get_rune(init, state.scope);
if (!rune || rune === '$effect.active') {
if (!rune || rune === '$effect.active' || rune === '$effect.root') {
if (init != null && is_hoistable_function(init)) {
const hoistable_function = visit(init);
state.hoisted.push(
@ -208,7 +208,6 @@ export const javascript_visitors_runes = {
// TODO
continue;
}
const args = /** @type {import('estree').CallExpression} */ (declarator.init).arguments;
const value =
args.length === 0
@ -292,13 +291,20 @@ export const javascript_visitors_runes = {
context.next();
},
CallExpression(node, { state, next }) {
CallExpression(node, { state, next, visit }) {
const rune = get_rune(node, state.scope);
if (rune === '$effect.active') {
return b.call('$.effect_active');
}
if (rune === '$effect.root') {
const args = /** @type {import('estree').Expression[]} */ (
node.arguments.map((arg) => visit(arg))
);
return b.call('$.user_root_effect', ...args);
}
next();
}
};

@ -70,7 +70,15 @@ export const ElementBindings = [
'indeterminate'
];
export const Runes = ['$state', '$props', '$derived', '$effect', '$effect.pre', '$effect.active'];
export const Runes = [
'$state',
'$props',
'$derived',
'$effect',
'$effect.pre',
'$effect.active',
'$effect.root'
];
/**
* Whitespace inside one of these elements will not result in

@ -1184,6 +1184,17 @@ export function user_effect(init) {
return effect;
}
/**
* @param {() => void | (() => void)} init
* @returns {() => void}
*/
export function user_root_effect(init) {
const effect = managed_render_effect(init);
return () => {
destroy_signal(effect);
};
}
/**
* @param {() => void | (() => void)} init
* @returns {import('./types.js').EffectSignal}

@ -36,7 +36,8 @@ export {
pop,
push,
reactive_import,
effect_active
effect_active,
user_root_effect
} from './client/runtime.js';
export * from './client/validate.js';

@ -90,6 +90,34 @@ declare namespace $effect {
* https://svelte-5-preview.vercel.app/docs/runes#$effect-active
*/
export function active(): boolean;
/**
* The `$effect.root` rune is an advanced feature that creates a non-tracked scope that doesn't auto-cleanup. This is useful for
* nested effects that you want to manually control. This rune also allows for creation of effects outside of the component
* initialisation phase.
*
* Example:
* ```svelte
* <script>
* let count = $state(0);
*
* const cleanup = $effect.root(() => {
* $effect(() => {
* console.log(count);
* })
*
* return () => {
* console.log('effect root cleanup');
* }
* });
* </script>
*
* <button onclick={() => cleanup()}>cleanup</button>
* ```
*
* https://svelte-5-preview.vercel.app/docs/runes#$effect-root
*/
export function root(fn: () => void | (() => void)): () => void;
}
/**

@ -0,0 +1,32 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
get props() {
return { log: [] };
},
async test({ assert, target, component }) {
const [b1, b2, b3] = target.querySelectorAll('button');
flushSync(() => {
b1.click();
b2.click();
});
assert.deepEqual(component.log, [0, 1]);
flushSync(() => {
b3.click();
});
assert.deepEqual(component.log, [0, 1, 'cleanup 1', 'cleanup 2']);
flushSync(() => {
b1.click();
b2.click();
});
assert.deepEqual(component.log, [0, 1, 'cleanup 1', 'cleanup 2']);
}
});

@ -0,0 +1,27 @@
<script>
let { log} = $props();
let x = $state(0);
let y = $state(0);
const cleanup = $effect.root(() => {
$effect(() => {
log.push(x);
});
const nested_cleanup = $effect.root(() => {
return () => {
log.push('cleanup 2') ;
}
});
return () => {
log.push('cleanup 1');
nested_cleanup();
}
});
</script>
<button on:click={() => x++}>{x}</button>
<button on:click={() => y++}>{y}</button>
<button on:click={() => cleanup()}>cleanup</button>

@ -186,6 +186,27 @@ The `$effect.active` rune is an advanced feature that tells you whether or not t
This allows you to (for example) add things like subscriptions without causing memory leaks, by putting them in child effects.
## `$effect.root`
The `$effect.root` rune is an advanced feature that creates a non-tracked scope that doesn't auto-cleanup. This is useful for
nested effects that you want to manually control. This rune also allows for creation of effects outside of the component initialisation phase.
```svelte
<script>
let count = $state(0);
const cleanup = $effect.root(() => {
$effect(() => {
console.log(count);
});
return () => {
console.log('effect root cleanup');
};
});
</script>
```
## `$props`
To declare component props, use the `$props` rune:

Loading…
Cancel
Save