diff --git a/.changeset/smooth-cameras-appear.md b/.changeset/smooth-cameras-appear.md new file mode 100644 index 000000000000..77dfb049f2cc --- /dev/null +++ b/.changeset/smooth-cameras-appear.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +fix: ensure correct each block element is moved during reconcilation diff --git a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js index 6a426db0d419..6892eb3b3c5c 100644 --- a/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js +++ b/packages/svelte/src/compiler/phases/3-transform/client/visitors/template.js @@ -1646,7 +1646,11 @@ export const template_visitors = { add_template(template_name, args); - body.push(b.var(id, b.call(template_name)), ...state.before_init, ...state.init); + body.push( + b.var(id, b.call(template_name, b.id('$$anchor'))), + ...state.before_init, + ...state.init + ); close = b.stmt(b.call('$.append', b.id('$$anchor'), id)); } else if (is_single_child_not_needing_template) { context.visit(trimmed[0], state); @@ -1684,7 +1688,7 @@ export const template_visitors = { if (use_comment_template) { // special case — we can use `$.comment` instead of creating a unique template - body.push(b.var(id, b.call('$.comment'))); + body.push(b.var(id, b.call('$.comment', b.id('$$anchor')))); } else { let flags = TEMPLATE_FRAGMENT; @@ -1697,7 +1701,7 @@ export const template_visitors = { b.literal(flags) ]); - body.push(b.var(id, b.call(template_name))); + body.push(b.var(id, b.call(template_name, b.id('$$anchor')))); } body.push(...state.before_init, ...state.init); diff --git a/packages/svelte/src/internal/client/dom/blocks/each.js b/packages/svelte/src/internal/client/dom/blocks/each.js index e20d4ec41fa9..45b0b0dd6459 100644 --- a/packages/svelte/src/internal/client/dom/blocks/each.js +++ b/packages/svelte/src/internal/client/dom/blocks/each.js @@ -291,7 +291,10 @@ function reconcile(array, state, anchor, render_fn, flags, get_key) { item = items.get(key); if (item === undefined) { - var child_anchor = current ? get_first_node(current.e) : anchor; + var effect_dom = current?.e.dom; + var child_anchor = /** @type {Node} */ ( + effect_dom ? (is_array(effect_dom) ? effect_dom[0] : effect_dom) : anchor + ); prev = create_item(child_anchor, prev, prev.next, value, key, i, render_fn, flags); @@ -486,7 +489,10 @@ function create_item(anchor, prev, next, value, key, index, render_fn, flags) { * @returns {import('#client').TemplateNode} */ function get_adjusted_first_node(dom, effect) { - if ((dom.nodeType === 3 && /** @type {Text} */ (dom).data === '') || dom.nodeType === 8) { + if ( + (dom.nodeType === 3 && /** @type {Text} */ (dom).data === '') || + (dom.nodeType === 8 && /** @type {Comment} */ (dom).data !== '[') + ) { var adjusted = effect.first; var next; while (adjusted !== null) { diff --git a/packages/svelte/src/internal/client/dom/blocks/html.js b/packages/svelte/src/internal/client/dom/blocks/html.js index 6873adf70ce2..8ab6d58d6b7e 100644 --- a/packages/svelte/src/internal/client/dom/blocks/html.js +++ b/packages/svelte/src/internal/client/dom/blocks/html.js @@ -82,7 +82,7 @@ function html_to_dom(target, effect, value, svg, mathml) { var child = /** @type {Text | Element | Comment} */ (node.firstChild); target.before(child); if (effect !== null) { - push_template_node(child, effect); + push_template_node(child, null, effect); } return child; } @@ -98,7 +98,7 @@ function html_to_dom(target, effect, value, svg, mathml) { } if (effect !== null) { - push_template_node(nodes, effect); + push_template_node(nodes, null, effect); } return nodes; diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-element.js b/packages/svelte/src/internal/client/dom/blocks/svelte-element.js index 6e0740b937b3..611eb6395fa1 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-element.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-element.js @@ -140,7 +140,7 @@ export function element(node, get_tag, is_svg, render_fn, get_namespace, locatio swap_block_dom(element_effect, prev_element, element); prev_element.remove(); } else { - push_template_node(element, element_effect); + push_template_node(element, null, element_effect); } if (render_fn) { diff --git a/packages/svelte/src/internal/client/dom/blocks/svelte-head.js b/packages/svelte/src/internal/client/dom/blocks/svelte-head.js index b00a3a242b1e..c5fb0f277115 100644 --- a/packages/svelte/src/internal/client/dom/blocks/svelte-head.js +++ b/packages/svelte/src/internal/client/dom/blocks/svelte-head.js @@ -1,7 +1,7 @@ import { hydrate_anchor, hydrate_nodes, hydrating, set_hydrate_nodes } from '../hydration.js'; import { empty } from '../operations.js'; import { block } from '../../reactivity/effects.js'; -import { HYDRATION_END, HYDRATION_START } from '../../../../constants.js'; +import { HYDRATION_START } from '../../../../constants.js'; /** * @type {Node | undefined} diff --git a/packages/svelte/src/internal/client/dom/template.js b/packages/svelte/src/internal/client/dom/template.js index 7342b71edadd..5ee3611b5e3d 100644 --- a/packages/svelte/src/internal/client/dom/template.js +++ b/packages/svelte/src/internal/client/dom/template.js @@ -9,10 +9,12 @@ import { queue_micro_task } from './task.js'; /** * @template {import("#client").TemplateNode | import("#client").TemplateNode[]} T * @param {T} dom + * @param {import("#client").TemplateNode | null} anchor * @param {import("#client").Effect} effect */ export function push_template_node( dom, + anchor, effect = /** @type {import('#client').Effect} */ (current_effect) ) { var current_dom = effect.dom; @@ -22,11 +24,23 @@ export function push_template_node( if (!is_array(current_dom)) { current_dom = effect.dom = [current_dom]; } + // If we have an existing anchor, then we should ensure that we insert the DOM contents + // before that anchor position. This ensures we match what is reflected on the document to + // as what is reflected in the effect.dom (we always insert before the anchor). + const anchor_index = anchor !== null ? current_dom.indexOf(anchor) : null; if (is_array(dom)) { - current_dom.push(...dom); + if (anchor_index !== null) { + current_dom.splice(anchor_index, 0, ...dom); + } else { + current_dom.push(...dom); + } } else { - current_dom.push(dom); + if (anchor_index !== null) { + current_dom.splice(anchor_index, 0, dom); + } else { + current_dom.push(dom); + } } } return dom; @@ -45,9 +59,9 @@ export function template(content, flags) { /** @type {Node} */ var node; - return () => { + return (/** @type {Element | Comment | null} */ prev_anchor) => { if (hydrating) { - push_template_node(is_fragment ? hydrate_nodes : hydrate_start); + push_template_node(is_fragment ? hydrate_nodes : hydrate_start, prev_anchor); return hydrate_start; } @@ -61,7 +75,8 @@ export function template(content, flags) { push_template_node( is_fragment ? /** @type {import('#client').TemplateNode[]} */ ([...clone.childNodes]) - : /** @type {import('#client').TemplateNode} */ (clone) + : /** @type {import('#client').TemplateNode} */ (clone), + prev_anchor ); return clone; @@ -106,9 +121,9 @@ export function ns_template(content, flags, ns = 'svg') { /** @type {Element | DocumentFragment} */ var node; - return () => { + return (/** @type {Element | Comment | null} */ prev_anchor) => { if (hydrating) { - push_template_node(is_fragment ? hydrate_nodes : hydrate_start); + push_template_node(is_fragment ? hydrate_nodes : hydrate_start, prev_anchor); return hydrate_start; } @@ -130,7 +145,8 @@ export function ns_template(content, flags, ns = 'svg') { push_template_node( is_fragment ? /** @type {import('#client').TemplateNode[]} */ ([...clone.childNodes]) - : /** @type {import('#client').TemplateNode} */ (clone) + : /** @type {import('#client').TemplateNode} */ (clone), + prev_anchor ); return clone; @@ -208,7 +224,7 @@ function run_scripts(node) { */ /*#__NO_SIDE_EFFECTS__*/ export function text(anchor) { - if (!hydrating) return push_template_node(empty()); + if (!hydrating) return push_template_node(empty(), null); var node = hydrate_start; @@ -218,21 +234,24 @@ export function text(anchor) { anchor.before((node = empty())); } - push_template_node(node); + push_template_node(node, null); return node; } -export function comment() { +/** + * @param {Element | Comment | null} prev_anchor + */ +export function comment(prev_anchor) { // we're not delegating to `template` here for performance reasons if (hydrating) { - push_template_node(hydrate_nodes); + push_template_node(hydrate_nodes, prev_anchor); return hydrate_start; } var frag = document.createDocumentFragment(); var anchor = empty(); frag.append(anchor); - push_template_node([anchor]); + push_template_node([anchor], prev_anchor); return frag; } diff --git a/packages/svelte/tests/migrate/samples/svelte-element/output.svelte b/packages/svelte/tests/migrate/samples/svelte-element/output.svelte index 09a9960f56ad..4dc05f351f81 100644 --- a/packages/svelte/tests/migrate/samples/svelte-element/output.svelte +++ b/packages/svelte/tests/migrate/samples/svelte-element/output.svelte @@ -3,4 +3,4 @@ - + \ No newline at end of file diff --git a/packages/svelte/tests/parser-modern/samples/snippets/output.json b/packages/svelte/tests/parser-modern/samples/snippets/output.json index 67d30db1b0b1..0926c2fb2669 100644 --- a/packages/svelte/tests/parser-modern/samples/snippets/output.json +++ b/packages/svelte/tests/parser-modern/samples/snippets/output.json @@ -27,8 +27,8 @@ "parameters": [ { "type": "Identifier", - "name": "msg", "start": 43, + "end": 46, "loc": { "start": { "line": 3, @@ -39,7 +39,7 @@ "column": 25 } }, - "end": 46, + "name": "msg", "typeAnnotation": { "type": "TSTypeAnnotation", "start": 46, diff --git a/packages/svelte/tests/runtime-runes/samples/each-updates-6/_config.js b/packages/svelte/tests/runtime-runes/samples/each-updates-6/_config.js new file mode 100644 index 000000000000..8bd2d17131df --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-updates-6/_config.js @@ -0,0 +1,27 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `
  • test (1)
  • test 2 (2)
  • test 3 (3)
`, + + async test({ assert, target }) { + const [btn1] = target.querySelectorAll('button'); + + flushSync(() => { + btn1.click(); + }); + + flushSync(() => { + btn1.click(); + }); + + flushSync(() => { + btn1.click(); + }); + + assert.htmlEqual( + target.innerHTML, + `
  • test (1)
  • test 2 (2)
  • test 3 (3)
` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/each-updates-6/main.svelte b/packages/svelte/tests/runtime-runes/samples/each-updates-6/main.svelte new file mode 100644 index 000000000000..a1b948ac0fb8 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-updates-6/main.svelte @@ -0,0 +1,26 @@ + + +{#snippet renderItem(item)} +
  • + {item.name} ({item.id}) + {#if item.color}{/if} +
  • +{/snippet} + +
      + {#each items as item (item.id)} + {@render renderItem(item)} + {/each} +
    + diff --git a/packages/svelte/tests/runtime-runes/samples/each-updates-7/_config.js b/packages/svelte/tests/runtime-runes/samples/each-updates-7/_config.js new file mode 100644 index 000000000000..7f1f5b65890d --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-updates-7/_config.js @@ -0,0 +1,27 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `
    • test (1)
    • test 2 (2)
    • test 3 (3)
    `, + + async test({ assert, target }) { + const [btn1] = target.querySelectorAll('button'); + + flushSync(() => { + btn1.click(); + }); + + flushSync(() => { + btn1.click(); + }); + + flushSync(() => { + btn1.click(); + }); + + assert.htmlEqual( + target.innerHTML, + `
    • test (1)
    • test 2 (2)
    • test 3 (3)
    ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/each-updates-7/main.svelte b/packages/svelte/tests/runtime-runes/samples/each-updates-7/main.svelte new file mode 100644 index 000000000000..df8b054a4258 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-updates-7/main.svelte @@ -0,0 +1,26 @@ + + +{#snippet renderItem(item)} +
  • + {item.name} ({item.id}) +
  • + {#if item.color}{/if} +{/snippet} + +
      + {#each items as item (item.id)} + {@render renderItem(item)} + {/each} +
    + diff --git a/packages/svelte/tests/runtime-runes/samples/each-updates-8/_config.js b/packages/svelte/tests/runtime-runes/samples/each-updates-8/_config.js new file mode 100644 index 000000000000..e675dcaf67c1 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-updates-8/_config.js @@ -0,0 +1,42 @@ +import { flushSync } from 'svelte'; +import { test } from '../../test'; + +export default test({ + html: `

    first

    message 1

    `, + + async test({ assert, target }) { + /** + * @type {{ click: () => void; }} + */ + let btn1; + + [btn1] = target.querySelectorAll('button'); + + flushSync(() => { + btn1.click(); + }); + + assert.htmlEqual( + target.innerHTML, + `

    first

    message 1

    message 2

    ` + ); + + await Promise.resolve(); + + assert.htmlEqual( + target.innerHTML, + `

    first

    message 1

    message 2

    ` + ); + + flushSync(() => { + btn1.click(); + }); + + await Promise.resolve(); + + assert.htmlEqual( + target.innerHTML, + `

    first

    message 1

    message 2

    message 3

    ` + ); + } +}); diff --git a/packages/svelte/tests/runtime-runes/samples/each-updates-8/main.svelte b/packages/svelte/tests/runtime-runes/samples/each-updates-8/main.svelte new file mode 100644 index 000000000000..869ccdc8dd78 --- /dev/null +++ b/packages/svelte/tests/runtime-runes/samples/each-updates-8/main.svelte @@ -0,0 +1,23 @@ + + + + +{#each messages as msg, i (`${msg.id}_${msg.tmpId ?? ""}`)} + {#if i === 0} +

    first

    + {/if} +

    {msg.content}

    +{/each} diff --git a/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js index 0e193af12d5f..5493a1318eaf 100644 --- a/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/bind-component-snippet/_expected/client/index.svelte.js @@ -7,14 +7,14 @@ var root = $.template(` `, 1); export default function Bind_component_snippet($$anchor) { var snippet = ($$anchor) => { - var fragment = root_1(); + var fragment = root_1($$anchor); $.append($$anchor, fragment); }; let value = $.source(''); const _snippet = snippet; - var fragment_1 = root(); + var fragment_1 = root($$anchor); var node = $.first_child(fragment_1); TextInput(node, { diff --git a/packages/svelte/tests/snapshot/samples/bind-this/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/bind-this/_expected/client/index.svelte.js index c766ee0a79b4..8ed7cb6ad9fd 100644 --- a/packages/svelte/tests/snapshot/samples/bind-this/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/bind-this/_expected/client/index.svelte.js @@ -2,7 +2,7 @@ import "svelte/internal/disclose-version"; import * as $ from "svelte/internal/client"; export default function Bind_this($$anchor) { - var fragment = $.comment(); + var fragment = $.comment($$anchor); var node = $.first_child(fragment); $.bind_this(Foo(node, { $$legacy: true }), ($$value) => foo = $$value, () => foo); diff --git a/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client/main.svelte.js b/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client/main.svelte.js index aa4d22d99a6d..b7324aabb099 100644 --- a/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client/main.svelte.js +++ b/packages/svelte/tests/snapshot/samples/dynamic-attributes-casing/_expected/client/main.svelte.js @@ -7,7 +7,7 @@ export default function Main($$anchor) { // needs to be a snapshot test because jsdom does auto-correct the attribute casing let x = 'test'; let y = () => 'test'; - var fragment = root(); + var fragment = root($$anchor); var div = $.first_child(fragment); var svg = $.sibling($.sibling(div, true)); var custom_element = $.sibling($.sibling(svg, true)); diff --git a/packages/svelte/tests/snapshot/samples/each-string-template/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/each-string-template/_expected/client/index.svelte.js index 5d5e47faf2d6..120ea6f4e0d2 100644 --- a/packages/svelte/tests/snapshot/samples/each-string-template/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/each-string-template/_expected/client/index.svelte.js @@ -2,7 +2,7 @@ import "svelte/internal/disclose-version"; import * as $ from "svelte/internal/client"; export default function Each_string_template($$anchor) { - var fragment = $.comment(); + var fragment = $.comment($$anchor); var node = $.first_child(fragment); $.each(node, 1, () => ['foo', 'bar', 'baz'], $.index, ($$anchor, thing, $$index) => { diff --git a/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js index 2d25ba9fd25c..f20857e33b21 100644 --- a/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/function-prop-no-getter/_expected/client/index.svelte.js @@ -9,7 +9,7 @@ export default function Function_prop_no_getter($$anchor) { } const plusOne = (num) => num + 1; - var fragment = $.comment(); + var fragment = $.comment($$anchor); var node = $.first_child(fragment); Button(node, { diff --git a/packages/svelte/tests/snapshot/samples/hello-world/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/hello-world/_expected/client/index.svelte.js index 92354d8f1483..ef0633e0d62f 100644 --- a/packages/svelte/tests/snapshot/samples/hello-world/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/hello-world/_expected/client/index.svelte.js @@ -4,7 +4,7 @@ import * as $ from "svelte/internal/client"; var root = $.template(`

    hello world

    `); export default function Hello_world($$anchor) { - var h1 = root(); + var h1 = root($$anchor); $.append($$anchor, h1); } \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/hmr/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/hmr/_expected/client/index.svelte.js index 6cb6845c1cce..fe88cae0f10e 100644 --- a/packages/svelte/tests/snapshot/samples/hmr/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/hmr/_expected/client/index.svelte.js @@ -4,7 +4,7 @@ import * as $ from "svelte/internal/client"; var root = $.template(`

    hello world

    `); function Hmr($$anchor) { - var h1 = root(); + var h1 = root($$anchor); $.append($$anchor, h1); } diff --git a/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/client/index.svelte.js index 7cb2415bf565..7b568077bb35 100644 --- a/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/state-proxy-literal/_expected/client/index.svelte.js @@ -13,7 +13,7 @@ var root = $.template(` `, 1); export default function State_proxy_literal($$anchor) { let str = $.source(''); let tpl = $.source(``); - var fragment = root(); + var fragment = root($$anchor); var input = $.first_child(fragment); $.remove_input_defaults(input); @@ -30,4 +30,4 @@ export default function State_proxy_literal($$anchor) { $.append($$anchor, fragment); } -$.delegate(["click"]); +$.delegate(["click"]); \ No newline at end of file diff --git a/packages/svelte/tests/snapshot/samples/svelte-element/_expected/client/index.svelte.js b/packages/svelte/tests/snapshot/samples/svelte-element/_expected/client/index.svelte.js index a4bbea582bf3..5caf3181488b 100644 --- a/packages/svelte/tests/snapshot/samples/svelte-element/_expected/client/index.svelte.js +++ b/packages/svelte/tests/snapshot/samples/svelte-element/_expected/client/index.svelte.js @@ -3,7 +3,7 @@ import * as $ from "svelte/internal/client"; export default function Svelte_element($$anchor, $$props) { let tag = $.prop($$props, "tag", 3, 'hr'); - var fragment = $.comment(); + var fragment = $.comment($$anchor); var node = $.first_child(fragment); $.element(node, tag, false); diff --git a/playgrounds/demo/server.js b/playgrounds/demo/server.js index 0a545e739758..82a75e70e7cd 100644 --- a/playgrounds/demo/server.js +++ b/playgrounds/demo/server.js @@ -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(``, appHtml) .replace(``, headHtml); - res.status(200).set({ 'Content-Type': 'text/html' }).end(html); + res.writeHead(200, { 'Content-Type': 'text/html' }).end(html); }); return { app, vite }; diff --git a/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md b/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md index 4a84e3a0f775..b83d347d2b3a 100644 --- a/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md +++ b/sites/svelte-5-preview/src/routes/docs/content/01-api/02-runes.md @@ -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