pull/15844/head
Rich Harris 3 months ago
commit da83c85420

@ -1,5 +0,0 @@
---
'svelte': patch
---
fix: abort and reschedule effect processing after state change in user effect

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: re-evaluate derived props during teardown

@ -1,5 +1,11 @@
# svelte
## 5.35.4
### Patch Changes
- fix: abort and reschedule effect processing after state change in user effect ([#16280](https://github.com/sveltejs/svelte/pull/16280))
## 5.35.3
### Patch Changes

@ -2,7 +2,7 @@
"name": "svelte",
"description": "Cybernetically enhanced web apps",
"license": "MIT",
"version": "5.35.3",
"version": "5.35.4",
"type": "module",
"types": "./types/index.d.ts",
"engines": {

@ -76,7 +76,7 @@ export function derived(fn) {
fn,
reactions: null,
rv: 0,
v: /** @type {V} */ (null),
v: /** @type {V} */ (UNINITIALIZED),
wv: 0,
parent: parent_derived ?? active_effect,
ac: null

@ -1,18 +1,26 @@
/** @import { Derived, Source } from './types.js' */
/** @import { ComponentContext } from '#client' */
/** @import { Derived, Effect, Source } from './types.js' */
import { DEV } from 'esm-env';
import {
PROPS_IS_BINDABLE,
PROPS_IS_IMMUTABLE,
PROPS_IS_LAZY_INITIAL,
PROPS_IS_RUNES,
PROPS_IS_UPDATED
PROPS_IS_UPDATED,
UNINITIALIZED
} from '../../../constants.js';
import { get_descriptor, is_function } from '../../shared/utils.js';
import { set, source, update } from './sources.js';
import { derived, derived_safe_equal } from './deriveds.js';
import { get, untrack } from '../runtime.js';
import {
active_effect,
get,
is_destroying_effect,
set_active_effect,
untrack
} from '../runtime.js';
import * as e from '../errors.js';
import { LEGACY_PROPS, STATE_SYMBOL } from '#client/constants';
import { DESTROYED, LEGACY_PROPS, STATE_SYMBOL } from '#client/constants';
import { proxy } from '../proxy.js';
import { capture_store_binding } from './store.js';
import { legacy_mode_flag } from '../../flags/index.js';
@ -92,7 +100,7 @@ export function rest_props(props, exclude, name) {
/**
* The proxy handler for legacy $$restProps and $$props
* @type {ProxyHandler<{ props: Record<string | symbol, unknown>, exclude: Array<string | symbol>, special: Record<string | symbol, (v?: unknown) => unknown>, version: Source<number> }>}}
* @type {ProxyHandler<{ props: Record<string | symbol, unknown>, exclude: Array<string | symbol>, special: Record<string | symbol, (v?: unknown) => unknown>, version: Source<number>, parent_effect: Effect }>}}
*/
const legacy_rest_props_handler = {
get(target, key) {
@ -102,17 +110,25 @@ const legacy_rest_props_handler = {
},
set(target, key, value) {
if (!(key in target.special)) {
// Handle props that can temporarily get out of sync with the parent
/** @type {Record<string, (v?: unknown) => unknown>} */
target.special[key] = prop(
{
get [key]() {
return target.props[key];
}
},
/** @type {string} */ (key),
PROPS_IS_UPDATED
);
var previous_effect = active_effect;
try {
set_active_effect(target.parent_effect);
// Handle props that can temporarily get out of sync with the parent
/** @type {Record<string, (v?: unknown) => unknown>} */
target.special[key] = prop(
{
get [key]() {
return target.props[key];
}
},
/** @type {string} */ (key),
PROPS_IS_UPDATED
);
} finally {
set_active_effect(previous_effect);
}
}
target.special[key](value);
@ -151,7 +167,19 @@ const legacy_rest_props_handler = {
* @returns {Record<string, unknown>}
*/
export function legacy_rest_props(props, exclude) {
return new Proxy({ props, exclude, special: {}, version: source(0) }, legacy_rest_props_handler);
return new Proxy(
{
props,
exclude,
special: {},
version: source(0),
// TODO this is only necessary because we need to track component
// destruction inside `prop`, because of `bind:this`, but it
// seems likely that we can simplify `bind:this` instead
parent_effect: /** @type {Effect} */ (active_effect)
},
legacy_rest_props_handler
);
}
/**
@ -366,16 +394,24 @@ export function prop(props, key, flags, fallback) {
// create a derived that we can write to locally.
// Or we are in legacy mode where we always create a derived to replicate that
// Svelte 4 did not trigger updates when a primitive value was updated to the same value.
var d = ((flags & PROPS_IS_IMMUTABLE) !== 0 ? derived : derived_safe_equal)(getter);
var overridden = false;
var d = ((flags & PROPS_IS_IMMUTABLE) !== 0 ? derived : derived_safe_equal)(() => {
overridden = false;
return getter();
});
// Capture the initial value if it's bindable
if (bindable) get(d);
var parent_effect = /** @type {Effect} */ (active_effect);
return function (/** @type {any} */ value, /** @type {boolean} */ mutation) {
if (arguments.length > 0) {
const new_value = mutation ? get(d) : runes && bindable ? proxy(value) : value;
set(d, new_value);
overridden = true;
if (fallback_value !== undefined) {
fallback_value = new_value;
@ -384,8 +420,12 @@ export function prop(props, key, flags, fallback) {
return value;
}
// TODO is this still necessary post-#16263?
if (has_destroyed_component_ctx(d)) {
// special case — avoid recalculating the derived if we're in a
// teardown function and the prop was overridden locally, or the
// component was already destroyed (this latter part is necessary
// because `bind:this` can read props after the component has
// been destroyed. TODO simplify `bind:this`
if ((is_destroying_effect && overridden) || (parent_effect.f & DESTROYED) !== 0) {
return d.v;
}

@ -45,6 +45,7 @@ import {
import * as w from './warnings.js';
import { Batch, batch_deriveds, flushSync, schedule_effect } from './reactivity/batch.js';
import { handle_error } from './error-handling.js';
import { UNINITIALIZED } from '../../constants.js';
export let is_updating_effect = false;
@ -617,7 +618,7 @@ export function get(signal) {
// if this is a derived, we may need to update it, but
// not if `batch_deriveds` is not null (meaning we're
// currently time travelling))
if (is_derived && batch_deriveds === null) {
if (is_derived && !is_destroying_effect && batch_deriveds === null) {
derived = /** @type {Derived} */ (signal);
if (check_dirtiness(derived)) {
@ -674,8 +675,25 @@ export function get(signal) {
}
}
if (is_destroying_effect && old_values.has(signal)) {
return old_values.get(signal);
if (is_destroying_effect) {
if (old_values.has(signal)) {
return old_values.get(signal);
}
if (is_derived) {
derived = /** @type {Derived} */ (signal);
var value = derived.v;
// if the derived is dirty, or depends on the values that just changed, re-execute
if ((derived.f & CLEAN) !== 0 || depends_on_old_values(derived)) {
value = execute_derived(derived);
}
old_values.set(derived, value);
return value;
}
}
// if we're time travelling, we don't want to update the
@ -698,6 +716,24 @@ export function get(signal) {
return signal.v;
}
/** @param {Derived} derived */
function depends_on_old_values(derived) {
if (derived.v === UNINITIALIZED) return true; // we don't know, so assume the worst
if (derived.deps === null) return false;
for (const dep of derived.deps) {
if (old_values.has(dep)) {
return true;
}
if ((dep.f & DERIVED) !== 0 && depends_on_old_values(/** @type {Derived} */ (dep))) {
return true;
}
}
return false;
}
/**
* Like `get`, but checks for `undefined`. Used for `var` declarations because they can be accessed before being declared
* @template V

@ -4,5 +4,5 @@
* The current version, as set in package.json.
* @type {string}
*/
export const VERSION = '5.35.3';
export const VERSION = '5.35.4';
export const PUBLIC_VERSION = '5';

@ -0,0 +1,22 @@
import { flushSync } from 'svelte';
import { test } from '../../test';
export default test({
async test({ assert, logs, target }) {
const [increment] = target.querySelectorAll('button');
flushSync(() => increment.click());
flushSync(() => increment.click());
flushSync(() => increment.click());
assert.deepEqual(logs, [
'count: 1',
'squared: 1',
'count: 2',
'squared: 4',
'count: 3',
'squared: 9',
'count: 4'
]);
}
});

@ -0,0 +1,20 @@
<script>
let count = $state(1);
let squared = $derived(count * count);
$effect(() => {
console.log(`count: ${count}`);
return () => {
console.log(`squared: ${squared}`);
};
});
</script>
<button onclick={() => count++}>increment</button>
<p>count: {count}</p>
{#if count % 2 === 0}
<p id="squared">squared: {squared}</p>
{/if}

@ -0,0 +1,15 @@
<script>
let { message, count } = $props();
$effect(() => () => {
console.log(count, message);
});
</script>
<p>{count}</p>
<!-- we need these so that the props are made into deriveds -->
<button disabled onclick={() => {
count += 1;
message += '!';
}}>update</button>

@ -0,0 +1,17 @@
import { test } from '../../test';
import { flushSync } from 'svelte';
export default test({
async test({ assert, target, logs }) {
const [increment, toggle] = target.querySelectorAll('button');
flushSync(() => toggle.click());
assert.deepEqual(logs, [0, 'hello']);
flushSync(() => toggle.click());
flushSync(() => increment.click());
flushSync(() => increment.click());
assert.deepEqual(logs, [0, 'hello', 1, 'hello']);
}
});

@ -0,0 +1,13 @@
<script>
import Component from "./Component.svelte";
let message = $state('hello');
let count = $state(0);
</script>
<button onclick={() => count++}>{count}</button>
<button onclick={() => message = message === 'hello' ? 'goodbye' : 'hello'}>{message}</button>
{#if count < 2 && message === 'hello'}
<Component {count} {message} />
{/if}
Loading…
Cancel
Save