Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: spread as a dependency for every prop #11290

Closed
Closed
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -627,6 +627,9 @@ function serialize_inline_component(node, component_name, context) {
/** @type {Array<import('estree').Property[] | import('estree').Expression>} */
const props_and_spreads = [];

/** @type {(import('estree').Expression | null)[]} */
const spreads_keys = [];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

all this stuff is unnecessary! we can create the derived inside spread_props

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duh 🤦🏻‍♂️ completely missed this


/** @type {import('estree').ExpressionStatement[]} */
const lets = [];

Expand Down Expand Up @@ -660,6 +663,7 @@ function serialize_inline_component(node, component_name, context) {
props.push(prop);
if (!current_is_props) {
props_and_spreads.push(props);
spreads_keys.push(null);
}
}
for (const attribute of node.attributes) {
Expand All @@ -684,8 +688,19 @@ function serialize_inline_component(node, component_name, context) {
}

props_and_spreads.push(b.thunk(value));
const keys = b.call(
'$.derived',
b.thunk(
b.call(
b.member(b.call('Object.keys', b.logical('??', value, b.object([]))), b.id('join')),
b.literal(',')
)
)
);
spreads_keys.push(b.thunk(keys));
} else {
props_and_spreads.push(expression);
spreads_keys.push(null);
}
} else if (attribute.type === 'Attribute') {
if (attribute.name.startsWith('--')) {
Expand Down Expand Up @@ -842,7 +857,8 @@ function serialize_inline_component(node, component_name, context) {
? b.object(/** @type {import('estree').Property[]} */ (props_and_spreads[0]) || [])
: b.call(
'$.spread_props',
...props_and_spreads.map((p) => (Array.isArray(p) ? b.object(p) : p))
b.array(props_and_spreads.map((p) => (Array.isArray(p) ? b.object(p) : p))),
b.array(spreads_keys)
);
/** @param {import('estree').Identifier} node_id */
let fn = (node_id) =>
Expand Down Expand Up @@ -2933,6 +2949,9 @@ export const template_visitors = {
/** @type {import('estree').Expression[]} */
const spreads = [];

/** @type {(import('estree').Expression | null)[]} */
const spreads_keys = [];

/** @type {import('estree').ExpressionStatement[]} */
const lets = [];

Expand All @@ -2943,9 +2962,18 @@ export const template_visitors = {

for (const attribute of node.attributes) {
if (attribute.type === 'SpreadAttribute') {
spreads.push(
b.thunk(/** @type {import('estree').Expression} */ (context.visit(attribute)))
const value = /** @type {import('estree').Expression} */ (context.visit(attribute));
const keys = b.call(
'$.derived',
b.thunk(
b.call(
b.member(b.call('Object.keys', b.logical('??', value, b.object([]))), b.id('join')),
trueadm marked this conversation as resolved.
Show resolved Hide resolved
b.literal(',')
)
)
);
spreads.push(b.thunk(value));
spreads_keys.push(b.thunk(keys));
} else if (attribute.type === 'Attribute') {
const [, value] = serialize_attribute_value(attribute.value, context);
if (attribute.name === 'name') {
Expand All @@ -2954,8 +2982,10 @@ export const template_visitors = {
} else if (attribute.name !== 'slot') {
if (attribute.metadata.dynamic) {
props.push(b.get(attribute.name, [b.return(value)]));
spreads_keys.push(null);
} else {
props.push(b.init(attribute.name, value));
spreads_keys.push(null);
}
}
} else if (attribute.type === 'LetDirective') {
Expand All @@ -2969,7 +2999,7 @@ export const template_visitors = {
const props_expression =
spreads.length === 0
? b.object(props)
: b.call('$.spread_props', b.object(props), ...spreads);
: b.call('$.spread_props', b.array([b.object(props), ...spreads]), b.array(spreads_keys));
const fallback =
node.fragment.nodes.length === 0
? b.literal(null)
Expand Down
43 changes: 35 additions & 8 deletions packages/svelte/src/internal/client/reactivity/props.js
Expand Up @@ -87,29 +87,55 @@ export function rest_props(props, exclude, name) {
* that looks like `() => { dynamic: props }, { static: prop }, ..` and wraps
* them so that the whole thing is passed to the component as the `$$props` argument.
* @template {Record<string | symbol, unknown>} T
* @type {ProxyHandler<{ props: Array<T | (() => T)> }>}}
* @type {ProxyHandler<{ props: (Array<T | (() => T)>), keys: (Array<(() => import('./types.js').Value<string>) | undefined>) }>}}
*/
const spread_props_handler = {
get(target, key) {
let i = target.props.length;
while (i--) {
let p = target.props[i];
if (is_function(p)) p = p();
if (typeof p === 'object' && p !== null && key in p) return p[key];
let obj = p;
// in case the prop is the spread prop calling the function
// will track that state as a dep even if the requested key
// is not in it. to avoid that we call the function in untrack
// and check on the object. If it's there we call the function again
// to track and then access the key
untrack(() => {
if (is_function(p)) obj = p();
});
const keys_function = target.keys[i];
if (typeof obj === 'object' && obj !== null && key in obj) {
if (is_function(p)) p = p();
return p[key];
} else if (keys_function && is_function(keys_function)) get(keys_function());
}
},
getOwnPropertyDescriptor(target, key) {
let i = target.props.length;
while (i--) {
let p = target.props[i];
if (is_function(p)) p = p();
if (typeof p === 'object' && p !== null && key in p) return get_descriptor(p, key);
let obj = p;
// in case the prop is the spread prop calling the function
// will track that state as a dep even if the requested key
// is not in it. to avoid that we call the function in untrack
// and check on the object. If it's there we call the function again
// to track and then access the key
untrack(() => {
if (is_function(p)) obj = p();
});
const keys_function = target.keys[i];
if (typeof obj === 'object' && obj !== null && key in obj) {
if (is_function(p)) p = p();
paoloricciuti marked this conversation as resolved.
Show resolved Hide resolved
return get_descriptor(p, key);
} else if (keys_function && is_function(keys_function)) get(keys_function());
}
},
has(target, key) {
for (let p of target.props) {
if (is_function(p)) p = p();
if (key in p) return true;
if (key in p) {
paoloricciuti marked this conversation as resolved.
Show resolved Hide resolved
return true;
}
}

return false;
Expand All @@ -131,10 +157,11 @@ const spread_props_handler = {

/**
* @param {Array<Record<string, unknown> | (() => Record<string, unknown>)>} props
* @param {(Array<(() => import('./types.js').Value<string>) | undefined>)} keys
* @returns {any}
*/
export function spread_props(...props) {
return new Proxy({ props }, spread_props_handler);
export function spread_props(props, keys) {
return new Proxy({ props, keys }, spread_props_handler);
}

/**
Expand Down
@@ -0,0 +1,7 @@
<script>
let { about, ...rest } = $props();

$effect(()=>{
console.log(about);
})
</script>
@@ -0,0 +1,71 @@
import { flushSync } from 'svelte';
import { test } from '../../test';

export default test({
// The component context class instance gets shared between tests, strangely, causing hydration to fail?

async test({ assert, target, logs }) {
/**
* @type {HTMLButtonElement | null}
*/
const no_keys_change = target.querySelector('#no-keys-change');
/**
* @type {HTMLButtonElement | null}
*/
const keys_change = target.querySelector('#keys-change');
/**
* @type {HTMLButtonElement | null}
*/
const no_keys_change_assign = target.querySelector('#no-keys-change-assign');
/**
* @type {HTMLButtonElement | null}
*/
const keys_change_assign = target.querySelector('#keys-change-assign');
/**
* @type {HTMLButtonElement | null}
*/
const keys_change_override = target.querySelector('#keys-change-override');
/**
* @type {HTMLButtonElement | null}
*/
const change_override_assign = target.querySelector('#change-override-assign');

assert.deepEqual(logs, ['something']);

flushSync(() => {
no_keys_change?.click();
});

assert.deepEqual(logs, ['something']);

flushSync(() => {
keys_change?.click();
});

assert.deepEqual(logs, ['something', 'something']);

flushSync(() => {
no_keys_change_assign?.click();
});

assert.deepEqual(logs, ['something', 'something']);

flushSync(() => {
keys_change_assign?.click();
});

assert.deepEqual(logs, ['something', 'something', 'something']);

flushSync(() => {
keys_change_override?.click();
});

assert.deepEqual(logs, ['something', 'something', 'something', 'else']);

flushSync(() => {
change_override_assign?.click();
});

assert.deepEqual(logs, ['something', 'something', 'something', 'else', 'another']);
}
});
@@ -0,0 +1,31 @@
<script lang="ts">
import Test from "./Test.svelte";
let rest = $state({});
</script>

<button id="no-keys-change" onclick={()=>{
rest = {};
}}></button>

<button id="keys-change" onclick={()=>{
rest = { foo: 1 };
}}></button>

<button id="no-keys-change-assign" onclick={()=>{
rest.foo = 1;
}}></button>

<button id="keys-change-assign" onclick={()=>{
rest.bar = 1;
}}></button>

<button id="keys-change-override" onclick={()=>{
rest = {about: "else"};
}}></button>

<button id="change-override-assign" onclick={()=>{
rest.about = "another";
}}></button>


<Test about="something" {...rest}></Test>