fix: prevent false-positive reactivity loss warning (#18373)

By checking that current/previous batch are null we filter out false
positives. `reactivity_loss_tracker` is only reset after a microtask, so
if a flush happens before that, we get warnings for things we shouldn't
warn on.

Fixes #18370

---------

Co-authored-by: Rich Harris <rich.harris@vercel.com>
pull/18357/head
Simon H 6 days ago committed by GitHub
parent bdb1a0f847
commit 3d5c4ab620
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: prevent false-positive reactivity loss warning

@ -181,10 +181,10 @@ export async function save(promise) {
* @returns {Promise<() => T>}
*/
export async function track_reactivity_loss(promise) {
var previous_async_effect = reactivity_loss_tracker;
var previous_reactivity_loss_tracker = reactivity_loss_tracker;
// Ensure that unrelated reads after an async operation is kicked off don't cause false positives
queueMicrotask(() => {
if (reactivity_loss_tracker === previous_async_effect) {
if (reactivity_loss_tracker === previous_reactivity_loss_tracker) {
set_reactivity_loss_tracker(null);
}
});
@ -192,12 +192,12 @@ export async function track_reactivity_loss(promise) {
var value = await promise;
return () => {
set_reactivity_loss_tracker(previous_async_effect);
set_reactivity_loss_tracker(previous_reactivity_loss_tracker);
// While this can result in false negatives it also guards against the more important
// false positives that would occur if this is the last in a chain of async operations,
// and the reactivity_loss_tracker would then stay around until the next async operation happens.
queueMicrotask(() => {
if (reactivity_loss_tracker === previous_async_effect) {
if (reactivity_loss_tracker === previous_reactivity_loss_tracker) {
set_reactivity_loss_tracker(null);
}
});

@ -51,6 +51,7 @@ import {
batch_values,
current_batch,
flushSync,
previous_batch,
schedule_effect
} from './reactivity/batch.js';
import { handle_error } from './error-handling.js';
@ -581,6 +582,11 @@ export function get(signal) {
if (
!untracking &&
reactivity_loss_tracker &&
// By checking that current/previous batch are null we filter out false positives.
// reactivity_loss_tracker is only reset after a microtask, so if a flush happens
// before that, we get warnings for things we shouldn't warn on.
current_batch === null &&
previous_batch === null &&
!reactivity_loss_tracker.warned &&
(reactivity_loss_tracker.effect.f & REACTION_IS_UPDATING) === 0 &&
!reactivity_loss_tracker.effect_deps.has(signal)

@ -0,0 +1,15 @@
import { tick } from 'svelte';
import { test } from '../../test';
export default test({
compileOptions: { dev: true },
async test({ assert, target, warnings }) {
await tick();
const [increment] = target.querySelectorAll('button');
increment.click();
await new Promise((resolve) => setTimeout(resolve, 10));
assert.htmlEqual(target.innerHTML, '<button>increment</button> 1 1');
assert.deepEqual(warnings, []);
}
});

@ -0,0 +1,16 @@
<script>
let x = $state(0);
let y = $state(0);
</script>
<button
onclick={() => {
x++;
queueMicrotask(() => queueMicrotask(() => y++));
}}
>
increment
</button>
{await x}
{y}
Loading…
Cancel
Save