Skip to content

Commit d408d20

Browse files
fix: replicate Svelte 4 props update detection in legacy mode (#11577)
* fix: replicate Svelte 4 props update detection in legacy mode fixes #11448 by wrapping props in deriveds * fix test * Update packages/svelte/src/compiler/phases/3-transform/client/utils.js Co-authored-by: Rich Harris <[email protected]> * dedicated flag * prettier --------- Co-authored-by: Rich Harris <[email protected]>
1 parent a0bdac8 commit d408d20

File tree

10 files changed

+79
-40
lines changed

10 files changed

+79
-40
lines changed

.changeset/empty-flowers-change.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"svelte": patch
3+
---
4+
5+
fix: replicate Svelte 4 props update detection in legacy mode

packages/svelte/src/compiler/phases/3-transform/client/utils.js

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -88,11 +88,7 @@ export function serialize_get_binding(node, state) {
8888
}
8989

9090
if (binding.kind === 'prop' || binding.kind === 'bindable_prop') {
91-
if (
92-
state.analysis.accessors ||
93-
(state.analysis.immutable ? binding.reassigned : binding.mutated) ||
94-
binding.initial
95-
) {
91+
if (!state.analysis.runes || binding.reassigned || binding.initial) {
9692
return b.call(node);
9793
}
9894

packages/svelte/src/compiler/phases/3-transform/client/visitors/javascript-legacy.js

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -93,24 +93,17 @@ export const javascript_visitors_legacy = {
9393
state.scope.get(declarator.id.name)
9494
);
9595

96-
if (
97-
state.analysis.accessors ||
98-
(state.analysis.immutable ? binding.reassigned : binding.mutated) ||
99-
declarator.init
100-
) {
101-
declarations.push(
102-
b.declarator(
103-
declarator.id,
104-
get_prop_source(
105-
binding,
106-
state,
107-
binding.prop_alias ?? declarator.id.name,
108-
declarator.init &&
109-
/** @type {import('estree').Expression} */ (visit(declarator.init))
110-
)
96+
declarations.push(
97+
b.declarator(
98+
declarator.id,
99+
get_prop_source(
100+
binding,
101+
state,
102+
binding.prop_alias ?? declarator.id.name,
103+
declarator.init && /** @type {import('estree').Expression} */ (visit(declarator.init))
111104
)
112-
);
113-
}
105+
)
106+
);
114107

115108
continue;
116109
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,9 @@ export const MAYBE_DIRTY = 1 << 10;
1111
export const INERT = 1 << 11;
1212
export const DESTROYED = 1 << 12;
1313
export const EFFECT_RAN = 1 << 13;
14-
1514
/** 'Transparent' effects do not create a transition boundary */
1615
export const EFFECT_TRANSPARENT = 1 << 14;
16+
/** Svelte 4 legacy mode props need to be handled with deriveds and be recognized elsewhere, hence the dedicated flag */
17+
export const LEGACY_DERIVED_PROP = 1 << 15;
1718

1819
export const STATE_SYMBOL = Symbol('$state');

packages/svelte/src/internal/client/reactivity/props.js

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@ import {
77
} from '../../../constants.js';
88
import { get_descriptor, is_function } from '../utils.js';
99
import { mutable_source, set, source } from './sources.js';
10-
import { derived } from './deriveds.js';
10+
import { derived, derived_safe_equal } from './deriveds.js';
1111
import { get, is_signals_recorded, untrack, update } from '../runtime.js';
1212
import { safe_equals } from './equality.js';
1313
import { inspect_fn } from '../dev/inspect.js';
1414
import * as e from '../errors.js';
15+
import { LEGACY_DERIVED_PROP } from '../constants.js';
1516

1617
/**
1718
* @param {((value?: number) => number)} fn
@@ -236,18 +237,28 @@ export function prop(props, key, flags, fallback) {
236237
if (setter) setter(prop_value);
237238
}
238239

239-
var getter = runes
240-
? () => {
241-
var value = /** @type {V} */ (props[key]);
242-
if (value === undefined) return get_fallback();
243-
fallback_dirty = true;
244-
return value;
245-
}
246-
: () => {
247-
var value = /** @type {V} */ (props[key]);
248-
if (value !== undefined) fallback_value = /** @type {V} */ (undefined);
249-
return value === undefined ? fallback_value : value;
250-
};
240+
/** @type {() => V} */
241+
var getter;
242+
if (runes) {
243+
getter = () => {
244+
var value = /** @type {V} */ (props[key]);
245+
if (value === undefined) return get_fallback();
246+
fallback_dirty = true;
247+
return value;
248+
};
249+
} else {
250+
// Svelte 4 did not trigger updates when a primitive value was updated to the same value.
251+
// Replicate that behavior through using a derived
252+
var derived_getter = (immutable ? derived : derived_safe_equal)(
253+
() => /** @type {V} */ (props[key])
254+
);
255+
derived_getter.f |= LEGACY_DERIVED_PROP;
256+
getter = () => {
257+
var value = get(derived_getter);
258+
if (value !== undefined) fallback_value = /** @type {V} */ (undefined);
259+
return value === undefined ? fallback_value : value;
260+
};
261+
}
251262

252263
// easy mode — prop is never written to
253264
if ((flags & PROPS_IS_UPDATED) === 0) {

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ import {
1515
BRANCH_EFFECT,
1616
STATE_SYMBOL,
1717
BLOCK_EFFECT,
18-
ROOT_EFFECT
18+
ROOT_EFFECT,
19+
LEGACY_DERIVED_PROP
1920
} from './constants.js';
2021
import { flush_tasks } from './dom/task.js';
2122
import { add_owner } from './dev/ownership.js';
@@ -835,7 +836,16 @@ export function invalidate_inner_signals(fn) {
835836
captured_signals = previous_captured_signals;
836837
}
837838
for (signal of captured) {
838-
mutate(signal, null /* doesnt matter */);
839+
// Go one level up because derived signals created as part of props in legacy mode
840+
if ((signal.f & LEGACY_DERIVED_PROP) !== 0) {
841+
for (const dep of /** @type {import('#client').Derived} */ (signal).deps || []) {
842+
if ((dep.f & DERIVED) === 0) {
843+
mutate(dep, null /* doesnt matter */);
844+
}
845+
}
846+
} else {
847+
mutate(signal, null /* doesnt matter */);
848+
}
839849
}
840850
}
841851

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<script>
2+
export let primitive;
3+
export let object;
4+
$: primitive && console.log('primitive');
5+
$: object && console.log('object');
6+
</script>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { test } from '../../test';
2+
3+
export default test({
4+
async test({ assert, logs, target }) {
5+
assert.deepEqual(logs, ['primitive', 'object']);
6+
await target.querySelector('button')?.click();
7+
assert.deepEqual(logs, ['primitive', 'object', 'object']);
8+
}
9+
});
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<script>
2+
import Nested from './Nested.svelte';
3+
4+
let value = { count: 1 };
5+
</script>
6+
7+
<button on:click={() => value = { count: 1 }}>reassign</button>
8+
<Nested primitive={value.count} object={value} />
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { test } from '../../test';
22

33
export default test({
4-
client: ['foo.bar.baz'],
5-
server: ['foo.bar.baz']
4+
client: ['bar.baz'],
5+
server: ['bar.baz']
66
});

0 commit comments

Comments
 (0)