mirror of https://github.com/sveltejs/svelte
fix: resolve effect_update_depth_exceeded with select bind:value in legacy mode (#17645)
## Summary Fixes #13768 `<select bind:value={derived.prop}>` in legacy (non-runes) components throws `effect_update_depth_exceeded` when the bound value comes from a `$:` reactive statement. **Root cause:** `setup_select_synchronization` created a `template_effect` that called `invalidate_inner_signals`, which reads and writes the same signals on every change — creating an infinite update loop when those signals feed back into derived state. **Fix:** Remove the effect-based synchronization entirely. Instead, populate `legacy_indirect_bindings` during the analyze phase for `<select bind:value>` elements, and call `invalidate_inner_signals` inline at the mutation point in `AssignmentExpression` — only when the binding is actually mutated, avoiding the read-write cycle. Based on the approach outlined in #16200. ## Changes - **`scope.js`**: Add `legacy_indirect_bindings` field to `Binding` class - **`RegularElement.js` (analyze)**: For `<select bind:value={foo}>`, collect scope references as indirect bindings on the bound variable - **`RegularElement.js` (transform)**: Remove `setup_select_synchronization` function and its call site - **`AssignmentExpression.js` (transform)**: When mutating a binding with indirect bindings, append `invalidate_inner_signals` call after the mutation ## Test plan - Added `binding-select-reactive-derived` test that reproduces the exact scenario from #13768 - All 3291 runtime-legacy tests pass (0 regressions) - All 2312 runtime-runes tests pass - All snapshot and compiler tests pass --------- Co-authored-by: Rich Harris <rich.harris@vercel.com>pull/17668/head
parent
3c6bb6faba
commit
684cdba253
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
'svelte': patch
|
||||||
|
---
|
||||||
|
|
||||||
|
fix: resolve `effect_update_depth_exceeded` when using `bind:value` on `<select>` with derived state in legacy mode
|
||||||
@ -0,0 +1,37 @@
|
|||||||
|
import { test } from '../../test';
|
||||||
|
|
||||||
|
export default test({
|
||||||
|
ssrHtml: `
|
||||||
|
<select>
|
||||||
|
<option selected="" value="">Select</option>
|
||||||
|
<option value="us">US</option>
|
||||||
|
<option value="uk">UK</option>
|
||||||
|
</select>
|
||||||
|
`,
|
||||||
|
|
||||||
|
async test({ assert, target, window, variant }) {
|
||||||
|
assert.htmlEqual(
|
||||||
|
target.innerHTML,
|
||||||
|
`
|
||||||
|
<select>
|
||||||
|
<option${variant === 'hydrate' ? ' selected=""' : ''} value="">Select</option>
|
||||||
|
<option value="us">US</option>
|
||||||
|
<option value="uk">UK</option>
|
||||||
|
</select>
|
||||||
|
`
|
||||||
|
);
|
||||||
|
|
||||||
|
const [select] = target.querySelectorAll('select');
|
||||||
|
const options = target.querySelectorAll('option');
|
||||||
|
|
||||||
|
assert.equal(select.value, '');
|
||||||
|
|
||||||
|
const change = new window.Event('change');
|
||||||
|
|
||||||
|
// Select "UK"
|
||||||
|
options[2].selected = true;
|
||||||
|
await select.dispatchEvent(change);
|
||||||
|
|
||||||
|
assert.equal(select.value, 'uk');
|
||||||
|
}
|
||||||
|
});
|
||||||
@ -0,0 +1,21 @@
|
|||||||
|
<script>
|
||||||
|
const default_details = {
|
||||||
|
country: '',
|
||||||
|
}
|
||||||
|
|
||||||
|
$: data = {
|
||||||
|
locked: false,
|
||||||
|
details: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
$: details = data.details ?? default_details
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<select
|
||||||
|
bind:value={details.country}
|
||||||
|
disabled={data.locked}
|
||||||
|
>
|
||||||
|
<option value="">Select</option>
|
||||||
|
<option value="us">US</option>
|
||||||
|
<option value="uk">UK</option>
|
||||||
|
</select>
|
||||||
Loading…
Reference in new issue