Merge branch 'main' into effect-dom-boundaries

pull/12215/head
Rich Harris 2 years ago
commit 83d60e8dfc

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix(types): export CompileResult and Warning

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: ensure element dir properties persist with text changes

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: disallow accessing internal Svelte props

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: make media bindings more robust

@ -0,0 +1,5 @@
---
'svelte': patch
---
feat: allow `let props = $props()` and optimize prop read access

@ -78,6 +78,10 @@
> Cannot use `$props()` more than once
## props_illegal_name
> Declaring or accessing a prop starting with `$$` is illegal (they are reserved for Svelte internals)
## props_invalid_identifier
> `$props()` can only be used with an object destructuring pattern

@ -276,6 +276,15 @@ export function props_duplicate(node) {
e(node, "props_duplicate", "Cannot use `$props()` more than once");
}
/**
* Declaring or accessing a prop starting with `$$` is illegal (they are reserved for Svelte internals)
* @param {null | number | NodeLike} node
* @returns {never}
*/
export function props_illegal_name(node) {
e(node, "props_illegal_name", "Declaring or accessing a prop starting with `$$` is illegal (they are reserved for Svelte internals)");
}
/**
* `$props()` can only be used with an object destructuring pattern
* @param {null | number | NodeLike} node

@ -31,6 +31,7 @@ import { hash } from './utils.js';
import { warn_unused } from './css/css-warn.js';
import { extract_svelte_ignore } from '../../utils/extract_svelte_ignore.js';
import { ignore_map, ignore_stack, pop_ignore, push_ignore } from '../../state.js';
import { equal } from '../../utils/assert.js';
/**
* @param {import('#compiler').Script | null} script
@ -969,34 +970,42 @@ const runes_scope_tweaker = {
if (rune === '$props') {
state.analysis.needs_props = true;
for (const property of /** @type {import('estree').ObjectPattern} */ (node.id).properties) {
if (property.type !== 'Property') continue;
const name =
property.value.type === 'AssignmentPattern'
? /** @type {import('estree').Identifier} */ (property.value.left).name
: /** @type {import('estree').Identifier} */ (property.value).name;
const alias =
property.key.type === 'Identifier'
? property.key.name
: String(/** @type {import('estree').Literal} */ (property.key).value);
let initial = property.value.type === 'AssignmentPattern' ? property.value.right : null;
const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(name));
binding.prop_alias = alias;
// rewire initial from $props() to the actual initial value, stripping $bindable() if necessary
if (
initial?.type === 'CallExpression' &&
initial.callee.type === 'Identifier' &&
initial.callee.name === '$bindable'
) {
binding.initial = /** @type {import('estree').Expression | null} */ (
initial.arguments[0] ?? null
);
binding.kind = 'bindable_prop';
} else {
binding.initial = initial;
if (node.id.type === 'Identifier') {
const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(node.id.name));
binding.initial = null; // else would be $props()
binding.kind = 'rest_prop';
} else {
equal(node.id.type, 'ObjectPattern');
for (const property of node.id.properties) {
if (property.type !== 'Property') continue;
const name =
property.value.type === 'AssignmentPattern'
? /** @type {import('estree').Identifier} */ (property.value.left).name
: /** @type {import('estree').Identifier} */ (property.value).name;
const alias =
property.key.type === 'Identifier'
? property.key.name
: String(/** @type {import('estree').Literal} */ (property.key).value);
let initial = property.value.type === 'AssignmentPattern' ? property.value.right : null;
const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(name));
binding.prop_alias = alias;
// rewire initial from $props() to the actual initial value, stripping $bindable() if necessary
if (
initial?.type === 'CallExpression' &&
initial.callee.type === 'Identifier' &&
initial.callee.name === '$bindable'
) {
binding.initial = /** @type {import('estree').Expression | null} */ (
initial.arguments[0] ?? null
);
binding.kind = 'bindable_prop';
} else {
binding.initial = initial;
}
}
}
}

@ -341,6 +341,14 @@ function validate_block_not_empty(node, context) {
* @type {import('zimmerframe').Visitors<import('#compiler').SvelteNode, import('./types.js').AnalysisState>}
*/
const validation = {
MemberExpression(node, context) {
if (node.object.type === 'Identifier' && node.property.type === 'Identifier') {
const binding = context.state.scope.get(node.object.name);
if (binding?.kind === 'rest_prop' && node.property.name.startsWith('$$')) {
e.props_illegal_name(node.property);
}
}
},
AssignmentExpression(node, context) {
validate_assignment(node, node.left, context.state);
},
@ -1244,7 +1252,7 @@ export const validation_runes = merge(validation, a11y_validators, {
e.rune_invalid_arguments(node, rune);
}
if (node.id.type !== 'ObjectPattern') {
if (node.id.type !== 'ObjectPattern' && node.id.type !== 'Identifier') {
e.props_invalid_identifier(node);
}
@ -1252,17 +1260,23 @@ export const validation_runes = merge(validation, a11y_validators, {
e.props_invalid_placement(node);
}
for (const property of node.id.properties) {
if (property.type === 'Property') {
if (property.computed) {
e.props_invalid_pattern(property);
}
if (node.id.type === 'ObjectPattern') {
for (const property of node.id.properties) {
if (property.type === 'Property') {
if (property.computed) {
e.props_invalid_pattern(property);
}
if (property.key.type === 'Identifier' && property.key.name.startsWith('$$')) {
e.props_illegal_name(property);
}
const value =
property.value.type === 'AssignmentPattern' ? property.value.left : property.value;
const value =
property.value.type === 'AssignmentPattern' ? property.value.left : property.value;
if (value.type !== 'Identifier') {
e.props_invalid_pattern(property);
if (value.type !== 'Identifier') {
e.props_invalid_pattern(property);
}
}
}
}

@ -9,6 +9,27 @@ export const global_visitors = {
if (node.name === '$$props') {
return b.id('$$sanitized_props');
}
// Optimize prop access: If it's a member read access, we can use the $$props object directly
const binding = state.scope.get(node.name);
if (
state.analysis.runes && // can't do this in legacy mode because the proxy does more than just read/write
binding !== null &&
node !== binding.node &&
binding.kind === 'rest_prop'
) {
const parent = path.at(-1);
const grand_parent = path.at(-2);
if (
parent?.type === 'MemberExpression' &&
!parent.computed &&
grand_parent?.type !== 'AssignmentExpression' &&
grand_parent?.type !== 'UpdateExpression'
) {
return b.id('$$props');
}
}
return serialize_get_binding(node, state);
}
},

@ -238,8 +238,6 @@ export const javascript_visitors_runes = {
}
if (rune === '$props') {
assert.equal(declarator.id.type, 'ObjectPattern');
/** @type {string[]} */
const seen = ['$$slots', '$$events', '$$legacy'];
@ -247,44 +245,58 @@ export const javascript_visitors_runes = {
seen.push('$$host');
}
for (const property of declarator.id.properties) {
if (property.type === 'Property') {
const key = /** @type {import('estree').Identifier | import('estree').Literal} */ (
property.key
);
const name = key.type === 'Identifier' ? key.name : /** @type {string} */ (key.value);
seen.push(name);
let id =
property.value.type === 'AssignmentPattern' ? property.value.left : property.value;
assert.equal(id.type, 'Identifier');
const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(id.name));
let initial =
binding.initial &&
/** @type {import('estree').Expression} */ (visit(binding.initial));
// We're adding proxy here on demand and not within the prop runtime function so that
// people not using proxied state anywhere in their code don't have to pay the additional bundle size cost
if (initial && binding.mutated && should_proxy_or_freeze(initial, state.scope)) {
initial = b.call('$.proxy', initial);
}
if (declarator.id.type === 'Identifier') {
/** @type {import('estree').Expression[]} */
const args = [b.id('$$props'), b.array(seen.map((name) => b.literal(name)))];
if (is_prop_source(binding, state)) {
declarations.push(b.declarator(id, get_prop_source(binding, state, name, initial)));
}
} else {
// RestElement
/** @type {import('estree').Expression[]} */
const args = [b.id('$$props'), b.array(seen.map((name) => b.literal(name)))];
if (state.options.dev) {
// include rest name, so we can provide informative error messages
args.push(
b.literal(/** @type {import('estree').Identifier} */ (property.argument).name)
if (state.options.dev) {
// include rest name, so we can provide informative error messages
args.push(b.literal(declarator.id.name));
}
declarations.push(b.declarator(declarator.id, b.call('$.rest_props', ...args)));
} else {
assert.equal(declarator.id.type, 'ObjectPattern');
for (const property of declarator.id.properties) {
if (property.type === 'Property') {
const key = /** @type {import('estree').Identifier | import('estree').Literal} */ (
property.key
);
const name = key.type === 'Identifier' ? key.name : /** @type {string} */ (key.value);
seen.push(name);
let id =
property.value.type === 'AssignmentPattern' ? property.value.left : property.value;
assert.equal(id.type, 'Identifier');
const binding = /** @type {import('#compiler').Binding} */ (state.scope.get(id.name));
let initial =
binding.initial &&
/** @type {import('estree').Expression} */ (visit(binding.initial));
// We're adding proxy here on demand and not within the prop runtime function so that
// people not using proxied state anywhere in their code don't have to pay the additional bundle size cost
if (initial && binding.mutated && should_proxy_or_freeze(initial, state.scope)) {
initial = b.call('$.proxy', initial);
}
if (is_prop_source(binding, state)) {
declarations.push(b.declarator(id, get_prop_source(binding, state, name, initial)));
}
} else {
// RestElement
/** @type {import('estree').Expression[]} */
const args = [b.id('$$props'), b.array(seen.map((name) => b.literal(name)))];
if (state.options.dev) {
// include rest name, so we can provide informative error messages
args.push(
b.literal(/** @type {import('estree').Identifier} */ (property.argument).name)
);
}
declarations.push(b.declarator(property.argument, b.call('$.rest_props', ...args)));
}
declarations.push(b.declarator(property.argument, b.call('$.rest_props', ...args)));
}
}

@ -1973,6 +1973,7 @@ export const template_visitors = {
let has_content_editable_binding = false;
let img_might_be_lazy = false;
let might_need_event_replaying = false;
let has_direction_attribute = false;
if (is_custom_element) {
// cloneNode is faster, but it does not instantiate the underlying class of the
@ -1988,6 +1989,9 @@ export const template_visitors = {
if (node.name === 'img' && attribute.name === 'loading') {
img_might_be_lazy = true;
}
if (attribute.name === 'dir') {
has_direction_attribute = true;
}
if (
(attribute.name === 'value' || attribute.name === 'checked') &&
!is_text_attribute(attribute)
@ -2188,6 +2192,14 @@ export const template_visitors = {
{ ...context, state }
);
if (has_direction_attribute) {
// This fixes an issue with Chromium where updates to text content within an element
// does not update the direction when set to auto. If we just re-assign the dir, this fixes it.
context.state.update.push(
b.stmt(b.assignment('=', b.member(node_id, b.id('dir')), b.member(node_id, b.id('dir'))))
);
}
if (child_locations.length > 0) {
// @ts-expect-error
location.push(child_locations);

@ -5,4 +5,4 @@ export type {
PreprocessorGroup,
Processed
} from './preprocess/public';
export type { CompileOptions, ModuleCompileOptions } from './types/index';
export type { CompileOptions, ModuleCompileOptions, CompileResult, Warning } from './types/index';

@ -22,7 +22,8 @@ function time_ranges_to_array(ranges) {
export function bind_current_time(media, get_value, update) {
/** @type {number} */
var raf_id;
var updating = false;
/** @type {number} */
var value;
// Ideally, listening to timeupdate would be enough, but it fires too infrequently for the currentTime
// binding, which is why we use a raf loop, too. We additionally still listen to timeupdate because
@ -34,22 +35,21 @@ export function bind_current_time(media, get_value, update) {
raf_id = requestAnimationFrame(callback);
}
updating = true;
update(media.currentTime);
var next_value = media.currentTime;
if (value !== next_value) {
update((value = next_value));
}
};
raf_id = requestAnimationFrame(callback);
media.addEventListener('timeupdate', callback);
render_effect(() => {
var value = get_value();
var next_value = Number(get_value());
// through isNaN we also allow number strings, which is more robust
if (!updating && !isNaN(/** @type {any} */ (value))) {
media.currentTime = /** @type {number} */ (value);
if (value !== next_value && !isNaN(/** @type {any} */ (next_value))) {
media.currentTime = value = next_value;
}
updating = false;
});
teardown(() => cancelAnimationFrame(raf_id));
@ -113,22 +113,21 @@ export function bind_ready_state(media, update) {
* @param {(playback_rate: number) => void} update
*/
export function bind_playback_rate(media, get_value, update) {
var updating = false;
// Needs to happen after the element is inserted into the dom, else playback will be set back to 1 by the browser.
// For hydration we could do it immediately but the additional code is not worth the lost microtask.
// Needs to happen after element is inserted into the dom (which is guaranteed by using effect),
// else playback will be set back to 1 by the browser
effect(() => {
var value = get_value();
var value = Number(get_value());
// through isNaN we also allow number strings, which is more robust
if (!isNaN(/** @type {any} */ (value)) && value !== media.playbackRate) {
updating = true;
media.playbackRate = /** @type {number} */ (value);
if (value !== media.playbackRate && !isNaN(value)) {
media.playbackRate = value;
}
});
// Start listening to ratechange events after the element is inserted into the dom,
// else playback will be set to 1 by the browser
effect(() => {
listen(media, ['ratechange'], () => {
if (!updating) update(media.playbackRate);
updating = false;
update(media.playbackRate);
});
});
}
@ -200,9 +199,7 @@ export function bind_paused(media, get_value, update) {
* @param {(volume: number) => void} update
*/
export function bind_volume(media, get_value, update) {
var updating = false;
var callback = () => {
updating = true;
update(media.volume);
};
@ -213,14 +210,11 @@ export function bind_volume(media, get_value, update) {
listen(media, ['volumechange'], callback, false);
render_effect(() => {
var value = get_value();
var value = Number(get_value());
// through isNaN we also allow number strings, which is more robust
if (!updating && !isNaN(/** @type {any} */ (value))) {
media.volume = /** @type {number} */ (value);
if (value !== media.volume && !isNaN(value)) {
media.volume = value;
}
updating = false;
});
}
@ -230,10 +224,7 @@ export function bind_volume(media, get_value, update) {
* @param {(muted: boolean) => void} update
*/
export function bind_muted(media, get_value, update) {
var updating = false;
var callback = () => {
updating = true;
update(media.muted);
};
@ -244,9 +235,8 @@ export function bind_muted(media, get_value, update) {
listen(media, ['volumechange'], callback, false);
render_effect(() => {
var value = get_value();
var value = !!get_value();
if (!updating) media.muted = !!value;
updating = false;
if (media.muted !== value) media.muted = value;
});
}

@ -78,6 +78,7 @@ const rest_props_handler = {
* @param {string} [name]
* @returns {Record<string, unknown>}
*/
/*#__NO_SIDE_EFFECTS__*/
export function rest_props(props, exclude, name) {
return new Proxy(
DEV ? { props, exclude, name, other: {}, to_proxy: [] } : { props, exclude },

@ -0,0 +1,9 @@
import { test } from '../../test';
export default test({
error: {
code: 'props_illegal_name',
message:
'Declaring or accessing a prop starting with `$$` is illegal (they are reserved for Svelte internals)'
}
});

@ -0,0 +1,9 @@
import { test } from '../../test';
export default test({
error: {
code: 'props_illegal_name',
message:
'Declaring or accessing a prop starting with `$$` is illegal (they are reserved for Svelte internals)'
}
});

@ -0,0 +1,4 @@
<script>
let props = $props();
props.$$slots;
</script>

@ -44,6 +44,7 @@ export function equal(a, b, message) {
/**
* @param {any} condition
* @param {string} [message]
* @returns {asserts condition}
*/
export function ok(condition, message) {
if (!condition) throw new Error(message || `Expected ${condition} to be truthy`);

@ -0,0 +1,29 @@
import { test, ok } from '../../assert';
export default test({
mode: ['client'],
async test({ assert, target }) {
const audio = target.querySelector('audio');
const button = target.querySelector('button');
ok(audio);
assert.equal(audio.muted, false);
audio.muted = true;
audio.dispatchEvent(new CustomEvent('volumechange'));
await new Promise((r) => setTimeout(r, 100));
assert.equal(audio.muted, true, 'event');
button?.click();
await new Promise((r) => setTimeout(r, 100));
assert.equal(audio.muted, false, 'click 1');
button?.click();
await new Promise((r) => setTimeout(r, 100));
assert.equal(audio.muted, true, 'click 2');
button?.click();
await new Promise((r) => setTimeout(r, 100));
assert.equal(audio.muted, false, 'click 3');
}
});

@ -0,0 +1,6 @@
<script>
let muted = $state(false);
</script>
<audio bind:muted></audio>
<button onclick={() => (muted = !muted)}>toggle</button>

@ -0,0 +1,29 @@
import { test, ok } from '../../assert';
export default test({
mode: ['client'],
async test({ assert, target }) {
const audio = target.querySelector('audio');
const button = target.querySelector('button');
ok(audio);
assert.equal(audio.playbackRate, 0.5);
audio.playbackRate = 1.0;
audio.dispatchEvent(new CustomEvent('ratechange'));
await new Promise((r) => setTimeout(r, 100));
assert.equal(audio.playbackRate, 1.0);
button?.click();
await new Promise((r) => setTimeout(r, 100));
assert.equal(audio.playbackRate, 2);
button?.click();
await new Promise((r) => setTimeout(r, 100));
assert.equal(audio.playbackRate, 3);
button?.click();
await new Promise((r) => setTimeout(r, 100));
assert.equal(audio.playbackRate, 4);
}
});

@ -0,0 +1,6 @@
<script>
let playbackRate = $state(0.5);
</script>
<audio bind:playbackRate></audio>
<button onclick={() => (playbackRate += 1)}>increment</button>

@ -0,0 +1,29 @@
import { test, ok } from '../../assert';
export default test({
mode: ['client'],
async test({ assert, target }) {
const audio = target.querySelector('audio');
const button = target.querySelector('button');
ok(audio);
assert.equal(audio.volume, 0.1);
audio.volume = 0.2;
audio.dispatchEvent(new CustomEvent('volumechange'));
await new Promise((r) => setTimeout(r, 100));
assert.equal(audio.volume, 0.2);
button?.click();
await new Promise((r) => setTimeout(r, 100));
assert.equal(audio.volume, 0.2 + 0.1); // JavaScript can't add floating point numbers correctly
button?.click();
await new Promise((r) => setTimeout(r, 100));
assert.equal(audio.volume, 0.2 + 0.1 + 0.1);
button?.click();
await new Promise((r) => setTimeout(r, 100));
assert.equal(audio.volume, 0.2 + 0.1 + 0.1 + 0.1);
}
});

@ -0,0 +1,6 @@
<script>
let volume = $state(0.1);
</script>
<audio bind:volume></audio>
<button onclick={() => (volume += 0.1)}>increment</button>

@ -0,0 +1,17 @@
import "svelte/internal/disclose-version";
import * as $ from "svelte/internal/client";
export default function Props_identifier($$anchor, $$props) {
$.push($$props, true);
let props = $.rest_props($$props, ["$$slots", "$$events", "$$legacy"]);
$$props.a;
props[a];
$$props.a.b;
$$props.a.b = true;
props.a = true;
props[a] = true;
props;
$.pop();
}

@ -0,0 +1,16 @@
import * as $ from "svelte/internal/server";
export default function Props_identifier($$payload, $$props) {
$.push();
let props = $$props;
props.a;
props[a];
props.a.b;
props.a.b = true;
props.a = true;
props[a] = true;
props;
$.pop();
}

@ -0,0 +1,10 @@
<script>
let props = $props();
props.a;
props[a];
props.a.b;
props.a.b = true;
props.a = true;
props[a] = true;
props;
</script>

@ -1899,7 +1899,7 @@ declare module 'svelte/compiler' {
attributes: Attribute[];
}
export { MarkupPreprocessor, Preprocessor, PreprocessorGroup, Processed, CompileOptions, ModuleCompileOptions, compile, compileModule, parse, walk, preprocess, CompileError, VERSION, migrate };
export { MarkupPreprocessor, Preprocessor, PreprocessorGroup, Processed, CompileOptions, ModuleCompileOptions, CompileResult, Warning, compile, compileModule, parse, walk, preprocess, CompileError, VERSION, migrate };
}
declare module 'svelte/easing' {

@ -1,3 +1,4 @@
// @ts-check
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
@ -22,7 +23,10 @@ async function createServer() {
app.use('*', async (req, res) => {
if (req.originalUrl !== '/') {
res.sendFile(path.resolve('./dist' + req.originalUrl));
res.writeHead(200, {
'Content-Type': 'application/javascript'
});
res.end(fs.createReadStream(path.resolve('./dist' + req.originalUrl)));
return;
}
@ -34,7 +38,7 @@ async function createServer() {
.replace(`<!--ssr-html-->`, appHtml)
.replace(`<!--ssr-head-->`, headHtml);
res.status(200).set({ 'Content-Type': 'text/html' }).end(html);
res.writeHead(200, { 'Content-Type': 'text/html' }).end(html);
});
return { app, vite };

@ -337,7 +337,7 @@ In general, `$effect` is best considered something of an escape hatch — useful
> For things that are more complicated than a simple expression like `count * 2`, you can also use [`$derived.by`](#$derived-by).
You might be tempted to do something convoluted with effects to link one value to another. The following example shows two inputs for "money spent" and "money left" that are connected to each other. If you update one, the other should update accordingly. Don't use effects for this ([demo](/#H4sIAAAAAAAACpVRy2rDMBD8lWXJwYE0dg-9KFYg31H3oNirIJBlYa1DjPG_F8l1XEop9LgzOzP7mFAbSwHF-4ROtYQCL97jAXn0sQh3skx4wNANfR2RMtS98XyuXMWWGLhjZUHCa1GcVix4cgwSdoEVU1bsn4wl_Y1I2kS6inekNdWcZXuQZ5giFDWpfwl5WYyT2fynbB1g1UWbTVbm2w6utOpKNq1TGucHhri6rLBX7kYVwtW4RtyVHUhOyXeGVj3klLxnyJP0i8lXNJUx6en-v6A48K85kTimpi0sYj-yAo-Wlh9FcL1LY4K3ahSgLT1OC3ZTXkBxfKN2uVC6T5LjAduuMdpQg4L7geaP-RNHPuClMQIAAA==)):
You might be tempted to do something convoluted with effects to link one value to another. The following example shows two inputs for "money spent" and "money left" that are connected to each other. If you update one, the other should update accordingly. Don't use effects for this ([demo](/#H4sIAAAAAAAACpVRQWrDMBD8ihA5ONDG7qEXxQ70HXUPir0KgrUsrHWIMf57pXWdlFIKPe6MZmZnNUtjEYJU77N0ugOp5Jv38knS5NMQroAEcQ79ODQJKUMzWE-n2tWEQIJ60igq8VIUxw0LHhxFbBdIE2TF_s4gmG8Ea5mM9A6MgYaybC-qk5gTlDT8fg15Xo3ZbPlTti2w6ZLNQ1bmjw6uRH0G5DqldX6MjWL1qpaDdheopThb16qrxhGqmX0X0elbNbP3InKWfjH5hvKYku7u_wtKC_-aw8Q9Jk0_UgJNCOvvJHC7SGuDRz0pYRBuxxW7aK9EcXiFbr0NX4bl8cO7vrXGQisVDSMsH8sniirsuSsCAAA=)):
```svelte
<script>
@ -361,11 +361,11 @@ You might be tempted to do something convoluted with effects to link one value t
<label>
<input type="range" bind:value={left} max={total} />
{left.value}/{total} left
{left}/{total} left
</label>
```
Instead, use callbacks where possible ([demo](/#H4sIAAAAAAAACo2SP2-DMBDFv8rp1CFR84cOXQhU6p6tY-ngwoEsGWPhI0pk8d0rG5yglqGj37v7veMJh7VUZDH9dKhFS5jiuzG4Q74Z_7AXUky4Q9sNfemVzJa9NPxW6IIVMXDHQkEOL0lyipo1pBlyeLIsmDbJ9u4oqhdG2A2mLrgedMmy0zCYSjB9eMaGtuC8WXBkPtOBRd8QHy5CDXSa3Jk7HbOfDgjWuAo_U71kz9vr6Bgc2X44orPjow2dKfFNKhSTSW0GBl9iXmAvdEMFQqDmLgBH6HQYyt3ie0doxTV3IWqEY2DN88eohqePvsf9O9mf_if4HMSVXD89NfEI99qvbMs3RdPv4MXYaSWtUeKWQq3oOlfZCJNCcnildlFgWMcdtl0la0kVptwPNH6NP_uzV0acAgAA)):
Instead, use callbacks where possible ([demo](/#H4sIAAAAAAAACo1SMW6EMBD8imWluFNyQIo0HERKf13KkMKB5WTJGAsvp0OIv8deMEEJRcqdmZ1ZjzzyWiqwPP0YuRYN8JS_GcOfOA7GD_YGCsHNtu270iOZLTtp8LXQBSpAhi0KxXL2nCTngFkDGh32YFEgHJLjyiioNwTtEunoutclylaz3lSOfPceBziy0ZMFBs9HiFB0V8DoJlQP55ldfOdjTvMBRE275hcn33gv2_vWITh4e3GwzuKfNnSmxBcoKiaT2vSuG1diXvBO6CsUnJFrPpLhxFpNonzcvHdijbjnI0VNLCavRR8HlEYfvcb9O9mf_if4QuBOLqnXWD_9SrU4KJg_ggdDm5W0RokhZbWC-1LiVZiUJdELNJvqaN39raatZC2h4il2PUyf0zcIbC-7lgIAAA==)):
```svelte
<script>
@ -405,7 +405,7 @@ Instead, use callbacks where possible ([demo](/#H4sIAAAAAAAACo2SP2-DMBDFv8rp1CFR
</label>
```
If you need to use bindings, for whatever reason (for example when you want some kind of "writable `$derived`"), consider using getters and setters to synchronise state ([demo](/#H4sIAAAAAAAACo2SQW-DMAyF_4pl7dBqXcsOu1CYtHtvO44dsmKqSCFExFRFiP8-xRCGth52tJ_9PecpA1bakMf0Y0CrasIU35zDHXLvQuGvZJhwh77p2nPoZP7casevhS3YEAM3rAzk8Jwkx9jzjixDDg-eFdMm2S6KoWolyK6ItuCqs2fWjYXOlYrpPTA2tIUhiAVH5iPtWbUX4v1VmY6Okzpzp2OepgNEGu_CT1St2fP2fXQ0juwwHNHZ4ScNmxn1RUaCybR1HUMIMS-wVfZCBYJQ80GAIzRWhvJh9d4RanXLB7Ea4SCsef4Qu1IG68Xu387h9D_GJ2ne8ZXpxTZUv1w994amjxCaMc1Se2dUn0Jl6DaHeFEuhWT_QvUqOlnHHdZNqStNJabcdjR-jt8IbC-7lgIAAA==)):
If you need to use bindings, for whatever reason (for example when you want some kind of "writable `$derived`"), consider using getters and setters to synchronise state ([demo](/#H4sIAAAAAAAACpVRQW7DIBD8CkI9JFIau4deiB2p7yg9kHhtIWGMYG3Fsvh7ARs3qnrpCWZGM8MuC22lAkfZ50K16IEy-mEMPVGcTQRuAoUQsBtGe49M5e5WGrxyzVEBEhxQKFKTt7K8ZM4Z0Bi4F4cC4VAeo7JpCtooLRFz7AIzCTXC4ZgpjhZwtHpLfl3TLqvoT-vpdt_0ZMy92TllVzx8AFXx83pdKXEDlQappDZjmCUMXXNqhe6AU3KTumGppV5StCe9eNRLivekSNZNKTKbYGza0_9XFPdzTvc_257kvTJyvxodzgrWP4pkXlEjnVFiZqRV8NiW0wnDSHl-hz4RPm0p2cO390MjWwkNZWhD5Zf_BkCCa6AxAgAA)):
```svelte
<script>
@ -491,7 +491,7 @@ Previously, you would have used `beforeUpdate`, which — like `afterUpdate` —
## `$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](/#H4sIAAAAAAAAE3XP0QrCMAwF0F-JRXAD595rLfgdzodRUyl0bVgzQcb-3VYFQfExl5tDMgvrPCYhT7MI_YBCiiOR2Aq-UxnSDT1jnlOcRlMSlczoiHUXOjYxpOhx5-O12rgAJg4UAwaGhDyR3Gxhjdai4V1v2N2wqus9tC3Y3ifMQjbehaqq4aBhLtEv_Or893icCsdLve-Caj8nBkU67zMO5HtGCfM3sKiWNKhV0zwVaBqd3x3ixVmHFyFLuJyXB-moOe8pAQAA)):
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](/#H4sIAAAAAAAACn3PQWrDMBAF0KtMRSA2xPFeUQU5R92FUUZBVB4N1rgQjO9eKSlkEcjyfz6PmVX5EDEr_bUqGidUWp2Z1UHJjWvIvxgFS85pmV1tTHZzYLEDDeIS5RTxGNO12QcClyZOhCSQURbW-wPs0Ht0cpR5dD-Brk3bnqDvwY8xYzGK8j9pmhY-Lay1eqUfm3eizEsFZWtPA5n-eSYZtkUQnDiOghrWV2IzPVswH113d6DrbHl6SpfgA16UruX2vf0BWo7W2y8BAAA=)):
```svelte
<script>
@ -548,6 +548,12 @@ To get all properties, use rest syntax:
let { a, b, c, ...everythingElse } = $props();
```
You can also use an identifier:
```js
let props = $props();
```
If you're using TypeScript, you can declare the prop types:
```ts

Loading…
Cancel
Save