Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/common-candles-sneeze.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

fix: don't mark deriveds while an effect is updating
3 changes: 2 additions & 1 deletion packages/svelte/src/internal/client/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ export const EFFECT_OFFSCREEN = 1 << 25;
/**
* Tells that we marked this derived and its reactions as visited during the "mark as (maybe) dirty"-phase.
* Will be lifted during execution of the derived and during checking its dirty state (both are necessary
* because a derived might be checked but not executed).
* because a derived might be checked but not executed). This is a pure performance optimization flag and
* should not be used for any other purpose!
*/
export const WAS_MARKED = 1 << 16;

Expand Down
10 changes: 7 additions & 3 deletions packages/svelte/src/internal/client/reactivity/sources.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ import {
ROOT_EFFECT,
ASYNC,
WAS_MARKED,
CONNECTED
CONNECTED,
REACTION_IS_UPDATING
} from '#client/constants';
import * as e from '../errors.js';
import { legacy_mode_flag, tracing_mode_flag } from '../../flags/index.js';
Expand Down Expand Up @@ -356,8 +357,11 @@ function mark_reactions(signal, status, updated_during_traversal) {
batch_values?.delete(derived);

if ((flags & WAS_MARKED) === 0) {
// Only connected deriveds can be reliably unmarked right away
if (flags & CONNECTED) {
// Only connected deriveds being executed outside the update cycle can be reliably unmarked right away
if (
flags & CONNECTED &&
(active_effect === null || (active_effect.f & REACTION_IS_UPDATING) === 0)
) {
reaction.f |= WAS_MARKED;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<script>
import { store } from "./store.svelte.js";

// This write marks the derived in main.svelte before it has reactions added to it.
// This test checks that this does not cause the WAS_MARKED logic to incorrectly skip marking the derived subsequently.
store.set("child-init-write", Math.random());
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { flushSync } from 'svelte';
import { test } from '../../test';

export default test({
async test({ assert, target }) {
const [show, hide] = target.querySelectorAll('button');

hide.click();
flushSync();
assert.htmlEqual(
target.innerHTML,
`
<button>show</button>
<button>hide</button>
`
);

show.click();
flushSync();
assert.htmlEqual(
target.innerHTML,
`
<button>show</button>
<button>hide</button>
<div>visible</div>
`
);
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<script>
import Child from "./Child.svelte";
import { store } from "./store.svelte.js";

const visible = $derived(store.get("visible"));
const visible2 = $derived(visible);
</script>

<button onclick={() => store.set("visible", true)}>show</button>
<button onclick={() => store.set("visible", false)}>hide</button>
{#if visible2}
<Child />
<div>visible</div>
{/if}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
class RawStore {
values = $state.raw({ visible: true });

get(key) {
return this.values[key];
}

set(key, value) {
this.values = { ...this.values, [key]: value };
}
}

export const store = new RawStore();