fix: access last safe value of prop on unmount

fix-props-wrong-value-onunmount
paoloricciuti 2 years ago
parent 9873443337
commit a73a619714

@ -0,0 +1,5 @@
---
'svelte': patch
---
fix: access last safe value of prop on unmount

@ -23,6 +23,8 @@ export interface ClientTransformState extends TransformState {
* us to rewrite `this.foo` as `this.#foo.value`
*/
readonly in_constructor: boolean;
readonly safe_props_ids?: Map<string, Expression>;
readonly safe_props_name?: string;
readonly transform: Record<
string,

@ -10,8 +10,24 @@ import { build_component } from './shared/component.js';
*/
export function Component(node, context) {
if (node.metadata.dynamic) {
let safe_props_ids = new Map();
const safe_props_name = context.state.scope.generate('$$safe_props');
// Handle dynamic references to what seems like static inline components
const component = build_component(node, '$$component', context, b.id('$$anchor'));
const component = build_component(
node,
'$$component',
{
...context,
state: {
...context.state,
safe_props_ids,
safe_props_name
}
},
b.id('$$anchor')
);
context.state.init.push(
b.stmt(
b.call(
@ -20,7 +36,19 @@ export function Component(node, context) {
// TODO use untrack here to not update when binding changes?
// Would align with Svelte 4 behavior, but it's arguably nicer/expected to update this
b.thunk(/** @type {Expression} */ (context.visit(b.member_id(node.name)))),
b.arrow([b.id('$$anchor'), b.id('$$component')], b.block([component]))
b.arrow(
[b.id('$$anchor'), b.id('$$component')],
b.block([
b.const(
safe_props_name,
b.call(
'$.safe_props',
b.object([...safe_props_ids].map(([name, id]) => b.get(name, [b.return(id)])))
)
),
component
])
)
)
)
);

@ -9,9 +9,9 @@ import { build_getter } from '../utils.js';
* @param {Context} context
*/
export function Identifier(node, context) {
const parent = /** @type {Node} */ (context.path.at(-1));
let parent = context.path.at(-1);
if (is_reference(node, parent)) {
if (is_reference(node, /** @type {Node} */ (parent))) {
if (node.name === '$$props') {
return b.id('$$sanitized_props');
}
@ -36,6 +36,36 @@ export function Identifier(node, context) {
}
}
return build_getter(node, context.state);
const getter = build_getter(node, context.state);
if (
// this means we are inside an if or as an attribute of a dynamic component
// and we want to access `$$safe_props` to allow for the component to access them
// after destructuring
context.state.safe_props_name != null &&
context.state.safe_props_ids != null &&
// the parent can either be a component/svelte component in that case we
// check if this identifier is one of the attributes
(((parent?.type === 'Component' || parent?.type === 'SvelteComponent') &&
parent.attributes.some(
(el) =>
(el.type === 'Attribute' &&
typeof el.value !== 'boolean' &&
!Array.isArray(el.value) &&
el.value.expression === node) ||
(el.type === 'BindDirective' && el.expression === node)
)) ||
// or a spread and we check the expression
(parent?.type === 'SpreadAttribute' && parent.expression === node)) &&
// we also don't want to transform bindings that are defined withing the if block
// itself (for example an each local variable)
!binding?.references[0].path.some((node) => node.type === 'IfBlock')
) {
// we store the getter in the safe props id and return an access to `$$safe_props.name`
context.state.safe_props_ids.set(node.name, getter);
return b.member(b.id(context.state.safe_props_name), b.id(node.name));
}
return getter;
}
}

@ -11,7 +11,29 @@ export function IfBlock(node, context) {
context.state.template.push('<!>');
const statements = [];
const consequent = /** @type {BlockStatement} */ (context.visit(node.consequent));
let safe_props_ids = new Map();
const safe_props_id = context.state.scope.generate('$$safe_props');
const consequent = /** @type {BlockStatement} */ (
context.visit(node.consequent, {
...context.state,
safe_props_ids,
safe_props_name: safe_props_id
})
);
if (consequent.body.length > 0 && safe_props_ids) {
consequent.body.unshift(
b.const(
safe_props_id,
b.call(
'$.safe_props',
b.object([...safe_props_ids].map(([name, id]) => b.get(name, [b.return(id)])))
)
)
);
}
const consequent_id = context.state.scope.generate('consequent');
statements.push(b.var(b.id(consequent_id), b.arrow([b.id('$$anchor')], consequent)));

@ -185,7 +185,7 @@ export function build_attribute_value(value, context, memoize = (value) => value
return { value: b.literal(chunk.data), has_state: false };
}
let expression = /** @type {Expression} */ (context.visit(chunk.expression));
let expression = /** @type {Expression} */ (context.visit(chunk.expression, context.state));
return {
value: memoize(expression, chunk.metadata.expression),

@ -118,7 +118,8 @@ export {
legacy_rest_props,
spread_props,
update_pre_prop,
update_prop
update_prop,
safe_props
} from './reactivity/props.js';
export {
invalidate_store,

@ -31,6 +31,7 @@ import {
import { proxy } from '../proxy.js';
import { capture_store_binding } from './store.js';
import { legacy_mode_flag } from '../../flags/index.js';
import { teardown } from './effects.js';
/**
* @param {((value?: number) => number)} fn
@ -416,3 +417,39 @@ export function prop(props, key, flags, fallback) {
return get(current_value);
};
}
/**
*
* @param {Record<string|symbol, unknown>} props
*/
export function safe_props(props) {
let unmounting = false;
teardown(() => {
unmounting = true;
});
const deriveds = new Map();
/**
* @type {Map<string|symbol, unknown>}
*/
const olds = new Map(untrack(() => Object.entries(props)));
return new Proxy(
{},
{
get(_, key) {
if (!deriveds.has(key)) {
deriveds.set(
key,
derived(() => {
if (unmounting) {
return olds.get(key);
}
olds.set(key, props[key]);
return props[key];
})
);
}
return get(deriveds.get(key));
}
}
);
}

Loading…
Cancel
Save