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

@ -70,7 +70,15 @@ export const ElementBindings = [
'indeterminate' '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 * Whitespace inside one of these elements will not result in

@ -1184,6 +1184,17 @@ export function user_effect(init) {
return effect; 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 * @param {() => void | (() => void)} init
* @returns {import('./types.js').EffectSignal} * @returns {import('./types.js').EffectSignal}

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

@ -90,6 +90,34 @@ declare namespace $effect {
* https://svelte-5-preview.vercel.app/docs/runes#$effect-active * https://svelte-5-preview.vercel.app/docs/runes#$effect-active
*/ */
export function active(): boolean; 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. 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` ## `$props`
To declare component props, use the `$props` rune: To declare component props, use the `$props` rune:

Loading…
Cancel
Save