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