Skip to content

Commit c141887

Browse files
committed
fix: traverse graph when time traveling
Often deriveds are currently unowned but through graph traversal during `is_dirty()` might reconnect to the graph, therefore having dependent effects register themselves as reactions. The problem prior to this fix was that the graph was not traversed to update the derived's dependencies and unowned status of deriveds in batch_values - the batch value was just returned -, which results in reactivity loss. #17024 (comment) contains a detailed rundown of such a case. The fix is to still traverse the graph, though not executing any deriveds in the process. Fixes #17024 Fixes #17049 (comment) (and therefore everything that was still buggy in that issue I think)
1 parent 0e709e3 commit c141887

File tree

5 files changed

+79
-20
lines changed

5 files changed

+79
-20
lines changed

packages/svelte/src/internal/client/dom/elements/transitions.js

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,7 @@
11
/** @import { AnimateFn, Animation, AnimationConfig, EachItem, Effect, TransitionFn, TransitionManager } from '#client' */
22
import { noop, is_function } from '../../../shared/utils.js';
33
import { effect } from '../../reactivity/effects.js';
4-
import {
5-
active_effect,
6-
active_reaction,
7-
set_active_effect,
8-
set_active_reaction,
9-
untrack
10-
} from '../../runtime.js';
4+
import { active_effect, untrack } from '../../runtime.js';
115
import { loop } from '../../loop.js';
126
import { should_intro } from '../../render.js';
137
import { current_each_item } from '../blocks/each.js';

packages/svelte/src/internal/client/runtime.js

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -148,24 +148,33 @@ export function increment_write_version() {
148148
/**
149149
* Determines whether a derived or effect is dirty.
150150
* If it is MAYBE_DIRTY, will set the status to CLEAN
151+
*
152+
* By default is_dirty executes deriveds and marks them as clean if not unowned etc.
153+
* But when multiple batches are active, batch_values may contain a value for a derived.
154+
* In this case we don't want to execute the derived (or its dependencies), but still
155+
* traverse the graph in order to reconnect unowned deriveds to their dependencies.
151156
* @param {Reaction} reaction
157+
* @param {boolean} [run_deriveds]
152158
* @returns {boolean}
153159
*/
154-
export function is_dirty(reaction) {
160+
export function is_dirty(reaction, run_deriveds = true) {
155161
var flags = reaction.f;
162+
var dirty = (flags & DIRTY) !== 0;
156163

157-
if ((flags & DIRTY) !== 0) {
164+
if (dirty && run_deriveds) {
158165
return true;
159166
}
160167

161-
if ((flags & MAYBE_DIRTY) !== 0) {
168+
// We don't need this above the DIRTY check because if it's dirty
169+
// the related derived update logic which is then called will also reset the flag
170+
if ((flags & DERIVED) !== 0) {
171+
reaction.f &= ~WAS_MARKED;
172+
}
173+
174+
if ((flags & MAYBE_DIRTY) !== 0 || dirty) {
162175
var dependencies = reaction.deps;
163176
var is_unowned = (flags & UNOWNED) !== 0;
164177

165-
if (flags & DERIVED) {
166-
reaction.f &= ~WAS_MARKED;
167-
}
168-
169178
if (dependencies !== null) {
170179
var i;
171180
var dependency;
@@ -208,7 +217,7 @@ export function is_dirty(reaction) {
208217
for (i = 0; i < length; i++) {
209218
dependency = dependencies[i];
210219

211-
if (is_dirty(/** @type {Derived} */ (dependency))) {
220+
if (is_dirty(/** @type {Derived} */ (dependency), run_deriveds) && run_deriveds) {
212221
update_derived(/** @type {Derived} */ (dependency));
213222
}
214223

@@ -220,7 +229,7 @@ export function is_dirty(reaction) {
220229

221230
// Unowned signals should never be marked as clean unless they
222231
// are used within an active_effect without skip_reaction
223-
if (!is_unowned || (active_effect !== null && !skip_reaction)) {
232+
if ((!is_unowned || (active_effect !== null && !skip_reaction)) && run_deriveds) {
224233
set_signal_status(reaction, CLEAN);
225234
}
226235
}
@@ -678,16 +687,17 @@ export function get(signal) {
678687
derived = /** @type {Derived} */ (signal);
679688

680689
if (batch_values?.has(derived)) {
690+
is_dirty(derived, false);
681691
return batch_values.get(derived);
682692
}
683693

684694
if (is_dirty(derived)) {
685695
update_derived(derived);
686696
}
687-
}
688-
689-
if (batch_values?.has(signal)) {
690-
return batch_values.get(signal);
697+
} else {
698+
if (batch_values?.has(signal)) {
699+
return batch_values.get(signal);
700+
}
691701
}
692702

693703
if ((signal.f & ERROR_VALUE) !== 0) {
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<script>
2+
let { double } = $props();
3+
double; // forces derived into UNOWNED mode
4+
</script>
5+
6+
<p>{double}</p>
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { tick } from 'svelte';
2+
import { test } from '../../test';
3+
4+
export default test({
5+
async test({ assert, target }) {
6+
const button = target.querySelector('button');
7+
8+
button?.click();
9+
await tick();
10+
11+
assert.htmlEqual(
12+
target.innerHTML,
13+
`
14+
<button>1</button>
15+
<p>2</p>
16+
`
17+
);
18+
19+
button?.click();
20+
await tick();
21+
22+
assert.htmlEqual(
23+
target.innerHTML,
24+
`
25+
<button>2</button>
26+
<p>4</p>
27+
`
28+
);
29+
}
30+
});
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<script>
2+
import Component from './Component.svelte';
3+
let count = $state(0);
4+
const double = $derived(count * 2);
5+
</script>
6+
7+
<svelte:boundary>
8+
{await new Promise((r) => {
9+
// long enough for the test to do all its other stuff while this is pending
10+
setTimeout(r, 10);
11+
})}
12+
{#snippet pending()}{/snippet}
13+
</svelte:boundary>
14+
15+
<button onclick={() => count += 1}>{count}</button>
16+
17+
{#if count > 0}
18+
<Component {double} />
19+
{/if}

0 commit comments

Comments
 (0)