mirror of https://github.com/sveltejs/svelte
fix: avoid false positives for reactivity loss warning (#18088)
Avoids two categories of false positives for reactivity loss warning: 1. if you have synchronously read signals already as part of invoking the async_derived function, then it shouldn't warn when these signals are read after an await if we know they haven't changed (we check the write version for that) 2. `track_reactivity_loss` kept the `reactivity_loss_tracker` around indefinitely, both when invoking the async operation as well as when it's finished. The former is buggy because while the async operation happens unrelated reads as part of other reactivity work can happen, the latter is buggy because if it's the last in a chain of awaits it's kept around until the next async work starts. Fixes https://github.com/sveltejs/kit/issues/15654 --------- Co-authored-by: Rich Harris <rich.harris@vercel.com> Co-authored-by: vercel[bot] <35613825+vercel[bot]@users.noreply.github.com>pull/18085/head
parent
0e9e76f292
commit
15588f5fbf
@ -0,0 +1,5 @@
|
||||
---
|
||||
'svelte': patch
|
||||
---
|
||||
|
||||
fix: avoid false positives for reactivity loss warning
|
||||
@ -0,0 +1,27 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
import { normalise_trace_logs } from '../../../helpers.js';
|
||||
|
||||
export default test({
|
||||
compileOptions: {
|
||||
dev: true
|
||||
},
|
||||
|
||||
async test({ assert, target, warnings }) {
|
||||
await new Promise((r) => setTimeout(r, 25));
|
||||
|
||||
const [count] = target.querySelectorAll('button');
|
||||
|
||||
count.click();
|
||||
await new Promise((r) => setTimeout(r, 25));
|
||||
|
||||
assert.deepEqual(normalise_trace_logs(warnings), [
|
||||
{
|
||||
log: 'Detected reactivity loss when reading `other`. This happens when state is read in an async function after an earlier `await`'
|
||||
},
|
||||
{
|
||||
log: 'Detected reactivity loss when reading `other`. This happens when state is read in an async function after an earlier `await`'
|
||||
}
|
||||
]);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,27 @@
|
||||
<script>
|
||||
let count = $state(0);
|
||||
let other = $state(0);
|
||||
|
||||
function delayed(value, ms = 1000) {
|
||||
return new Promise((f) => setTimeout(() => f(value), ms))
|
||||
}
|
||||
|
||||
async function foo() {
|
||||
await new Promise(r => setTimeout(r, 10));
|
||||
}
|
||||
|
||||
async function bar() {
|
||||
const value = await delayed(count, 10);
|
||||
other; // should trigger warning
|
||||
return value;
|
||||
}
|
||||
|
||||
async function get() {
|
||||
foo();
|
||||
return await bar();
|
||||
}
|
||||
</script>
|
||||
|
||||
<button onclick={() => count++}>{count}</button>
|
||||
<button onclick={() => other++}>{other}</button>
|
||||
{await get()}
|
||||
@ -0,0 +1,21 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
compileOptions: {
|
||||
dev: true
|
||||
},
|
||||
|
||||
async test({ assert, target, warnings }) {
|
||||
await tick();
|
||||
|
||||
const [x, y] = target.querySelectorAll('button');
|
||||
|
||||
y.click();
|
||||
await tick();
|
||||
x.click();
|
||||
await new Promise((r) => setTimeout(r, 15));
|
||||
|
||||
assert.equal(warnings.length, 0);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,16 @@
|
||||
<script>
|
||||
let x = $state(0);
|
||||
let y = $state(0);
|
||||
async function foo(x) {
|
||||
if (x) {
|
||||
await 1; // restores reactivity loss warning context
|
||||
await new Promise(r => setTimeout(r, 10)) // saves reactivity loss warning context; should not keep it while running
|
||||
}
|
||||
return x
|
||||
}
|
||||
</script>
|
||||
|
||||
{x} {await foo(y)}
|
||||
|
||||
<button onclick={() => x += 1}>x++</button>
|
||||
<button onclick={() => y += 1}>y++</button>
|
||||
@ -0,0 +1,19 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
compileOptions: {
|
||||
dev: true
|
||||
},
|
||||
|
||||
async test({ assert, target, warnings }) {
|
||||
await tick();
|
||||
|
||||
const [count] = target.querySelectorAll('button');
|
||||
|
||||
count.click();
|
||||
await tick();
|
||||
|
||||
assert.equal(warnings.length, 0);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,14 @@
|
||||
<script>
|
||||
let count = $state(0);
|
||||
async function delay(c) {
|
||||
if (c) {
|
||||
await new Promise(r => setTimeout(r));
|
||||
count; // count already read synchronously; should not result in reacitive loss warning
|
||||
}
|
||||
return c;
|
||||
}
|
||||
</script>
|
||||
|
||||
{await delay(count)}
|
||||
|
||||
<button onclick={() => count += 1}>count++</button>
|
||||
@ -0,0 +1,19 @@
|
||||
import { tick } from 'svelte';
|
||||
import { test } from '../../test';
|
||||
|
||||
export default test({
|
||||
compileOptions: {
|
||||
dev: true
|
||||
},
|
||||
|
||||
async test({ assert, target, warnings }) {
|
||||
await new Promise((r) => setTimeout(r, 5));
|
||||
|
||||
const [count] = target.querySelectorAll('button');
|
||||
|
||||
count.click();
|
||||
await tick();
|
||||
|
||||
assert.equal(warnings.length, 0);
|
||||
}
|
||||
});
|
||||
@ -0,0 +1,14 @@
|
||||
<script>
|
||||
let count = $state(0);
|
||||
|
||||
async function run() {
|
||||
await new Promise(r => setTimeout(r));
|
||||
}
|
||||
async function get() {
|
||||
run();
|
||||
return 1;
|
||||
}
|
||||
</script>
|
||||
|
||||
<button onclick={() => count++}>{count}</button>
|
||||
{await get()}
|
||||
Loading…
Reference in new issue