Skip to content

Commit a73a619

Browse files
committed
fix: access last safe value of prop on unmount
1 parent 9873443 commit a73a619

File tree

8 files changed

+133
-8
lines changed

8 files changed

+133
-8
lines changed

.changeset/tasty-pears-travel.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: access last safe value of prop on unmount

packages/svelte/src/compiler/phases/3-transform/client/types.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ export interface ClientTransformState extends TransformState {
2323
* us to rewrite `this.foo` as `this.#foo.value`
2424
*/
2525
readonly in_constructor: boolean;
26+
readonly safe_props_ids?: Map<string, Expression>;
27+
readonly safe_props_name?: string;
2628

2729
readonly transform: Record<
2830
string,

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

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,24 @@ import { build_component } from './shared/component.js';
1010
*/
1111
export function Component(node, context) {
1212
if (node.metadata.dynamic) {
13+
let safe_props_ids = new Map();
14+
15+
const safe_props_name = context.state.scope.generate('$$safe_props');
16+
1317
// Handle dynamic references to what seems like static inline components
14-
const component = build_component(node, '$$component', context, b.id('$$anchor'));
18+
const component = build_component(
19+
node,
20+
'$$component',
21+
{
22+
...context,
23+
state: {
24+
...context.state,
25+
safe_props_ids,
26+
safe_props_name
27+
}
28+
},
29+
b.id('$$anchor')
30+
);
1531
context.state.init.push(
1632
b.stmt(
1733
b.call(
@@ -20,7 +36,19 @@ export function Component(node, context) {
2036
// TODO use untrack here to not update when binding changes?
2137
// Would align with Svelte 4 behavior, but it's arguably nicer/expected to update this
2238
b.thunk(/** @type {Expression} */ (context.visit(b.member_id(node.name)))),
23-
b.arrow([b.id('$$anchor'), b.id('$$component')], b.block([component]))
39+
b.arrow(
40+
[b.id('$$anchor'), b.id('$$component')],
41+
b.block([
42+
b.const(
43+
safe_props_name,
44+
b.call(
45+
'$.safe_props',
46+
b.object([...safe_props_ids].map(([name, id]) => b.get(name, [b.return(id)])))
47+
)
48+
),
49+
component
50+
])
51+
)
2452
)
2553
)
2654
);

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

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ import { build_getter } from '../utils.js';
99
* @param {Context} context
1010
*/
1111
export function Identifier(node, context) {
12-
const parent = /** @type {Node} */ (context.path.at(-1));
12+
let parent = context.path.at(-1);
1313

14-
if (is_reference(node, parent)) {
14+
if (is_reference(node, /** @type {Node} */ (parent))) {
1515
if (node.name === '$$props') {
1616
return b.id('$$sanitized_props');
1717
}
@@ -36,6 +36,36 @@ export function Identifier(node, context) {
3636
}
3737
}
3838

39-
return build_getter(node, context.state);
39+
const getter = build_getter(node, context.state);
40+
41+
if (
42+
// this means we are inside an if or as an attribute of a dynamic component
43+
// and we want to access `$$safe_props` to allow for the component to access them
44+
// after destructuring
45+
context.state.safe_props_name != null &&
46+
context.state.safe_props_ids != null &&
47+
// the parent can either be a component/svelte component in that case we
48+
// check if this identifier is one of the attributes
49+
(((parent?.type === 'Component' || parent?.type === 'SvelteComponent') &&
50+
parent.attributes.some(
51+
(el) =>
52+
(el.type === 'Attribute' &&
53+
typeof el.value !== 'boolean' &&
54+
!Array.isArray(el.value) &&
55+
el.value.expression === node) ||
56+
(el.type === 'BindDirective' && el.expression === node)
57+
)) ||
58+
// or a spread and we check the expression
59+
(parent?.type === 'SpreadAttribute' && parent.expression === node)) &&
60+
// we also don't want to transform bindings that are defined withing the if block
61+
// itself (for example an each local variable)
62+
!binding?.references[0].path.some((node) => node.type === 'IfBlock')
63+
) {
64+
// we store the getter in the safe props id and return an access to `$$safe_props.name`
65+
context.state.safe_props_ids.set(node.name, getter);
66+
return b.member(b.id(context.state.safe_props_name), b.id(node.name));
67+
}
68+
69+
return getter;
4070
}
4171
}

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

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,29 @@ export function IfBlock(node, context) {
1111
context.state.template.push('<!>');
1212
const statements = [];
1313

14-
const consequent = /** @type {BlockStatement} */ (context.visit(node.consequent));
14+
let safe_props_ids = new Map();
15+
16+
const safe_props_id = context.state.scope.generate('$$safe_props');
17+
18+
const consequent = /** @type {BlockStatement} */ (
19+
context.visit(node.consequent, {
20+
...context.state,
21+
safe_props_ids,
22+
safe_props_name: safe_props_id
23+
})
24+
);
25+
26+
if (consequent.body.length > 0 && safe_props_ids) {
27+
consequent.body.unshift(
28+
b.const(
29+
safe_props_id,
30+
b.call(
31+
'$.safe_props',
32+
b.object([...safe_props_ids].map(([name, id]) => b.get(name, [b.return(id)])))
33+
)
34+
)
35+
);
36+
}
1537
const consequent_id = context.state.scope.generate('consequent');
1638

1739
statements.push(b.var(b.id(consequent_id), b.arrow([b.id('$$anchor')], consequent)));

packages/svelte/src/compiler/phases/3-transform/client/visitors/shared/element.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ export function build_attribute_value(value, context, memoize = (value) => value
185185
return { value: b.literal(chunk.data), has_state: false };
186186
}
187187

188-
let expression = /** @type {Expression} */ (context.visit(chunk.expression));
188+
let expression = /** @type {Expression} */ (context.visit(chunk.expression, context.state));
189189

190190
return {
191191
value: memoize(expression, chunk.metadata.expression),

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,8 @@ export {
118118
legacy_rest_props,
119119
spread_props,
120120
update_pre_prop,
121-
update_prop
121+
update_prop,
122+
safe_props
122123
} from './reactivity/props.js';
123124
export {
124125
invalidate_store,

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

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
import { proxy } from '../proxy.js';
3232
import { capture_store_binding } from './store.js';
3333
import { legacy_mode_flag } from '../../flags/index.js';
34+
import { teardown } from './effects.js';
3435

3536
/**
3637
* @param {((value?: number) => number)} fn
@@ -416,3 +417,39 @@ export function prop(props, key, flags, fallback) {
416417
return get(current_value);
417418
};
418419
}
420+
421+
/**
422+
*
423+
* @param {Record<string|symbol, unknown>} props
424+
*/
425+
export function safe_props(props) {
426+
let unmounting = false;
427+
teardown(() => {
428+
unmounting = true;
429+
});
430+
const deriveds = new Map();
431+
/**
432+
* @type {Map<string|symbol, unknown>}
433+
*/
434+
const olds = new Map(untrack(() => Object.entries(props)));
435+
return new Proxy(
436+
{},
437+
{
438+
get(_, key) {
439+
if (!deriveds.has(key)) {
440+
deriveds.set(
441+
key,
442+
derived(() => {
443+
if (unmounting) {
444+
return olds.get(key);
445+
}
446+
olds.set(key, props[key]);
447+
return props[key];
448+
})
449+
);
450+
}
451+
return get(deriveds.get(key));
452+
}
453+
}
454+
);
455+
}

0 commit comments

Comments
 (0)