From 8a6d5ed34b3532062592bfb303e21aae3356dfcc Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 30 Jun 2025 21:23:25 -0400 Subject: [PATCH 01/13] simplify props --- .../src/internal/client/reactivity/props.js | 28 ++----------------- 1 file changed, 3 insertions(+), 25 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js index f3111361c051..03f4938fbe2d 100644 --- a/packages/svelte/src/internal/client/reactivity/props.js +++ b/packages/svelte/src/internal/client/reactivity/props.js @@ -362,52 +362,30 @@ export function prop(props, key, flags, fallback) { // hard mode. this is where it gets ugly — the value in the child should // synchronize with the parent, but it should also be possible to temporarily // set the value to something else locally. - var from_child = false; - var was_from_child = false; // The derived returns the current value. The underlying mutable // source is written to from various places to persist this value. - var inner_current_value = mutable_source(prop_value); - var current_value = derived(() => { - var parent_value = getter(); - var child_value = get(inner_current_value); - - if (from_child) { - from_child = false; - was_from_child = true; - return child_value; - } - - was_from_child = false; - return (inner_current_value.v = parent_value); - }); + var current_value = (immutable ? derived : derived_safe_equal)(getter); // Ensure we eagerly capture the initial value if it's bindable if (bindable) { get(current_value); } - if (!immutable) current_value.equals = safe_equals; - return function (/** @type {any} */ value, /** @type {boolean} */ mutation) { // legacy nonsense — need to ensure the source is invalidated when necessary // also needed for when handling inspect logic so we can inspect the correct source signal if (captured_signals !== null) { - // set this so that we don't reset to the parent value if `d` - // is invalidated because of `invalidate_inner_signals` (rather - // than because the parent or child value changed) - from_child = was_from_child; // invoke getters so that signals are picked up by `invalidate_inner_signals` getter(); - get(inner_current_value); + get(current_value); } if (arguments.length > 0) { const new_value = mutation ? get(current_value) : runes && bindable ? proxy(value) : value; if (!current_value.equals(new_value)) { - from_child = true; - set(inner_current_value, new_value); + set(current_value, new_value); // To ensure the fallback value is consistent when used with proxies, we // update the local fallback_value, but only if the fallback is actively used if (fallback_used && fallback_value !== undefined) { From e0ebcc8b5d4368917d34b9062d1a30d62282f820 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 30 Jun 2025 21:33:04 -0400 Subject: [PATCH 02/13] simplify --- .../svelte/src/internal/client/reactivity/props.js | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js index 03f4938fbe2d..1d27c2fb0f1a 100644 --- a/packages/svelte/src/internal/client/reactivity/props.js +++ b/packages/svelte/src/internal/client/reactivity/props.js @@ -373,35 +373,25 @@ export function prop(props, key, flags, fallback) { } return function (/** @type {any} */ value, /** @type {boolean} */ mutation) { - // legacy nonsense — need to ensure the source is invalidated when necessary - // also needed for when handling inspect logic so we can inspect the correct source signal - if (captured_signals !== null) { - // invoke getters so that signals are picked up by `invalidate_inner_signals` - getter(); - get(current_value); - } - if (arguments.length > 0) { const new_value = mutation ? get(current_value) : runes && bindable ? proxy(value) : value; if (!current_value.equals(new_value)) { set(current_value, new_value); + // To ensure the fallback value is consistent when used with proxies, we // update the local fallback_value, but only if the fallback is actively used if (fallback_used && fallback_value !== undefined) { fallback_value = new_value; } - if (has_destroyed_component_ctx(current_value)) { - return value; - } - untrack(() => get(current_value)); // force a synchronisation immediately } return value; } + // TODO is this still necessary post-#16263? if (has_destroyed_component_ctx(current_value)) { return current_value.v; } From 9506889f80358f9c52dc8e4ae3b8981b0f3b5534 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 30 Jun 2025 21:41:54 -0400 Subject: [PATCH 03/13] tweak --- .../src/internal/client/reactivity/props.js | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js index 1d27c2fb0f1a..37e53b5a6dc1 100644 --- a/packages/svelte/src/internal/client/reactivity/props.js +++ b/packages/svelte/src/internal/client/reactivity/props.js @@ -277,11 +277,17 @@ export function prop(props, key, flags, fallback) { // or `createClassComponent(Component, props)` var is_entry_props = STATE_SYMBOL in props || LEGACY_PROPS in props; - var setter = - (bindable && - (get_descriptor(props, key)?.set ?? - (is_entry_props && key in props && ((v) => (props[key] = v))))) || - undefined; + /** @type {((v: V) => void) | undefined} */ + var setter; + + /** @type {() => V} */ + var getter; + + if (bindable) { + setter = + get_descriptor(props, key)?.set ?? + (is_entry_props && key in props ? (v) => (props[key] = v) : undefined); + } var fallback_value = /** @type {V} */ (fallback); var fallback_dirty = true; @@ -307,11 +313,9 @@ export function prop(props, key, flags, fallback) { } prop_value = get_fallback(); - if (setter) setter(prop_value); + setter?.(prop_value); } - /** @type {() => V} */ - var getter; if (runes) { getter = () => { var value = /** @type {V} */ (props[key]); From 2a16f17de717e6ad6859d2e35d8786f43762adb7 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 30 Jun 2025 21:48:35 -0400 Subject: [PATCH 04/13] reorder a bit --- .../src/internal/client/reactivity/props.js | 63 ++++++++++--------- 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js index 37e53b5a6dc1..101e8f2dd242 100644 --- a/packages/svelte/src/internal/client/reactivity/props.js +++ b/packages/svelte/src/internal/client/reactivity/props.js @@ -264,18 +264,24 @@ export function prop(props, key, flags, fallback) { var runes = !legacy_mode_flag || (flags & PROPS_IS_RUNES) !== 0; var bindable = (flags & PROPS_IS_BINDABLE) !== 0; var lazy = (flags & PROPS_IS_LAZY_INITIAL) !== 0; - var is_store_sub = false; - var prop_value; - if (bindable) { - [prop_value, is_store_sub] = capture_store_binding(() => /** @type {V} */ (props[key])); - } else { - prop_value = /** @type {V} */ (props[key]); - } + var fallback_value = /** @type {V} */ (fallback); + var fallback_dirty = true; + var fallback_used = false; + + var get_fallback = () => { + fallback_used = true; - // Can be the case when someone does `mount(Component, props)` with `let props = $state({...})` - // or `createClassComponent(Component, props)` - var is_entry_props = STATE_SYMBOL in props || LEGACY_PROPS in props; + if (fallback_dirty) { + fallback_dirty = false; + + fallback_value = lazy + ? untrack(/** @type {() => V} */ (fallback)) + : /** @type {V} */ (fallback); + } + + return fallback_value; + }; /** @type {((v: V) => void) | undefined} */ var setter; @@ -284,36 +290,31 @@ export function prop(props, key, flags, fallback) { var getter; if (bindable) { + // Can be the case when someone does `mount(Component, props)` with `let props = $state({...})` + // or `createClassComponent(Component, props)` + var is_entry_props = STATE_SYMBOL in props || LEGACY_PROPS in props; + setter = get_descriptor(props, key)?.set ?? (is_entry_props && key in props ? (v) => (props[key] = v) : undefined); } - var fallback_value = /** @type {V} */ (fallback); - var fallback_dirty = true; - var fallback_used = false; + var initial_value; + var is_store_sub = false; - var get_fallback = () => { - fallback_used = true; - if (fallback_dirty) { - fallback_dirty = false; - if (lazy) { - fallback_value = untrack(/** @type {() => V} */ (fallback)); - } else { - fallback_value = /** @type {V} */ (fallback); - } - } + if (bindable) { + [initial_value, is_store_sub] = capture_store_binding(() => /** @type {V} */ (props[key])); + } else { + initial_value = /** @type {V} */ (props[key]); + } - return fallback_value; - }; + if (initial_value === undefined && fallback !== undefined) { + initial_value = get_fallback(); - if (prop_value === undefined && fallback !== undefined) { - if (setter && runes) { - e.props_invalid_value(key); + if (setter) { + if (runes) e.props_invalid_value(key); + setter(initial_value); } - - prop_value = get_fallback(); - setter?.(prop_value); } if (runes) { From b141f60a7f45770939011605115ec41516dfdb58 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 30 Jun 2025 21:53:24 -0400 Subject: [PATCH 05/13] simplify --- packages/svelte/src/internal/client/constants.js | 2 -- .../src/internal/client/reactivity/props.js | 15 ++++----------- packages/svelte/src/internal/client/runtime.js | 13 +------------ 3 files changed, 5 insertions(+), 25 deletions(-) diff --git a/packages/svelte/src/internal/client/constants.js b/packages/svelte/src/internal/client/constants.js index dd3d1b2df636..cd5e0d224444 100644 --- a/packages/svelte/src/internal/client/constants.js +++ b/packages/svelte/src/internal/client/constants.js @@ -15,8 +15,6 @@ export const DESTROYED = 1 << 14; export const EFFECT_RAN = 1 << 15; /** 'Transparent' effects do not create a transition boundary */ export const EFFECT_TRANSPARENT = 1 << 16; -/** Svelte 4 legacy mode props need to be handled with deriveds and be recognized elsewhere, hence the dedicated flag */ -export const LEGACY_DERIVED_PROP = 1 << 17; export const INSPECT_EFFECT = 1 << 18; export const HEAD_EFFECT = 1 << 19; export const EFFECT_HAS_DERIVED = 1 << 20; diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js index 101e8f2dd242..42a905f6682f 100644 --- a/packages/svelte/src/internal/client/reactivity/props.js +++ b/packages/svelte/src/internal/client/reactivity/props.js @@ -8,12 +8,11 @@ import { PROPS_IS_UPDATED } from '../../../constants.js'; import { get_descriptor, is_function } from '../../shared/utils.js'; -import { mutable_source, set, source, update } from './sources.js'; +import { set, source, update } from './sources.js'; import { derived, derived_safe_equal } from './deriveds.js'; -import { get, captured_signals, untrack } from '../runtime.js'; -import { safe_equals } from './equality.js'; +import { get, untrack } from '../runtime.js'; import * as e from '../errors.js'; -import { LEGACY_DERIVED_PROP, LEGACY_PROPS, STATE_SYMBOL } from '#client/constants'; +import { 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'; @@ -326,14 +325,8 @@ export function prop(props, key, flags, fallback) { return value; }; } else { - // Svelte 4 did not trigger updates when a primitive value was updated to the same value. - // Replicate that behavior through using a derived - var derived_getter = (immutable ? derived : derived_safe_equal)( - () => /** @type {V} */ (props[key]) - ); - derived_getter.f |= LEGACY_DERIVED_PROP; getter = () => { - var value = get(derived_getter); + var value = /** @type {V} */ (props[key]); if (value !== undefined) fallback_value = /** @type {V} */ (undefined); return value === undefined ? fallback_value : value; }; diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 3e70537d7cbb..5a798ba3e996 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -20,7 +20,6 @@ import { STATE_SYMBOL, BLOCK_EFFECT, ROOT_EFFECT, - LEGACY_DERIVED_PROP, DISCONNECTED, EFFECT_IS_UPDATING, STALE_REACTION @@ -863,17 +862,7 @@ export function invalidate_inner_signals(fn) { var captured = capture_signals(() => untrack(fn)); for (var signal of captured) { - // Go one level up because derived signals created as part of props in legacy mode - if ((signal.f & LEGACY_DERIVED_PROP) !== 0) { - for (const dep of /** @type {Derived} */ (signal).deps || []) { - if ((dep.f & DERIVED) === 0) { - // Use internal_set instead of set here and below to avoid mutation validation - internal_set(dep, dep.v); - } - } - } else { - internal_set(signal, signal.v); - } + internal_set(signal, signal.v); } } From ab1ea276259c83335bc30a4bc94e30393a21ca26 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 30 Jun 2025 21:54:31 -0400 Subject: [PATCH 06/13] unused --- packages/svelte/src/internal/client/reactivity/props.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js index 42a905f6682f..919ad37f35fa 100644 --- a/packages/svelte/src/internal/client/reactivity/props.js +++ b/packages/svelte/src/internal/client/reactivity/props.js @@ -382,8 +382,6 @@ export function prop(props, key, flags, fallback) { if (fallback_used && fallback_value !== undefined) { fallback_value = new_value; } - - untrack(() => get(current_value)); // force a synchronisation immediately } return value; From bdd5897e1346992ff47c4e19596e79da1afa2651 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 30 Jun 2025 21:58:58 -0400 Subject: [PATCH 07/13] more --- .../svelte/src/internal/client/reactivity/props.js | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js index 919ad37f35fa..f09883f5b4e7 100644 --- a/packages/svelte/src/internal/client/reactivity/props.js +++ b/packages/svelte/src/internal/client/reactivity/props.js @@ -374,14 +374,12 @@ export function prop(props, key, flags, fallback) { if (arguments.length > 0) { const new_value = mutation ? get(current_value) : runes && bindable ? proxy(value) : value; - if (!current_value.equals(new_value)) { - set(current_value, new_value); + set(current_value, new_value); - // To ensure the fallback value is consistent when used with proxies, we - // update the local fallback_value, but only if the fallback is actively used - if (fallback_used && fallback_value !== undefined) { - fallback_value = new_value; - } + // To ensure the fallback value is consistent when used with proxies, we + // update the local fallback_value, but only if the fallback is actively used + if (fallback_used && fallback_value !== undefined) { + fallback_value = new_value; } return value; From 125388655952f7f0d7bce3793c4a5ad9b57bfaa0 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 30 Jun 2025 22:08:23 -0400 Subject: [PATCH 08/13] more --- .../src/internal/client/reactivity/props.js | 46 +++++++++---------- 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js index f09883f5b4e7..9420b8e94fef 100644 --- a/packages/svelte/src/internal/client/reactivity/props.js +++ b/packages/svelte/src/internal/client/reactivity/props.js @@ -285,9 +285,6 @@ export function prop(props, key, flags, fallback) { /** @type {((v: V) => void) | undefined} */ var setter; - /** @type {() => V} */ - var getter; - if (bindable) { // Can be the case when someone does `mount(Component, props)` with `let props = $state({...})` // or `createClassComponent(Component, props)` @@ -316,6 +313,9 @@ export function prop(props, key, flags, fallback) { } } + /** @type {() => V} */ + var getter; + if (runes) { getter = () => { var value = /** @type {V} */ (props[key]); @@ -332,15 +332,16 @@ export function prop(props, key, flags, fallback) { }; } - // easy mode — prop is never written to - if ((flags & PROPS_IS_UPDATED) === 0 && runes) { + // prop is never written to — we only need a getter + if (runes && (flags & PROPS_IS_UPDATED) === 0) { return getter; } - // intermediate mode — prop is written to, but the parent component had - // `bind:foo` which means we can just call `$$props.foo = value` directly + // prop is written to, but the parent component had `bind:foo` which + // means we can just call `$$props.foo = value` directly if (setter) { var legacy_parent = props.$$legacy; + return function (/** @type {any} */ value, /** @type {boolean} */ mutation) { if (arguments.length > 0) { // We don't want to notify if the value was mutated and the parent is in runes mode. @@ -350,31 +351,26 @@ export function prop(props, key, flags, fallback) { if (!runes || !mutation || legacy_parent || is_store_sub) { /** @type {Function} */ (setter)(mutation ? getter() : value); } + return value; - } else { - return getter(); } + + return getter(); }; } - // hard mode. this is where it gets ugly — the value in the child should - // synchronize with the parent, but it should also be possible to temporarily - // set the value to something else locally. + // prop is written to, but there's no binding, which means we + // create a derived that we can write to locally + var d = (immutable ? derived : derived_safe_equal)(getter); - // The derived returns the current value. The underlying mutable - // source is written to from various places to persist this value. - var current_value = (immutable ? derived : derived_safe_equal)(getter); - - // Ensure we eagerly capture the initial value if it's bindable - if (bindable) { - get(current_value); - } + // Capture the initial value if it's bindable + if (bindable) get(d); return function (/** @type {any} */ value, /** @type {boolean} */ mutation) { if (arguments.length > 0) { - const new_value = mutation ? get(current_value) : runes && bindable ? proxy(value) : value; + const new_value = mutation ? get(d) : runes && bindable ? proxy(value) : value; - set(current_value, new_value); + set(d, new_value); // To ensure the fallback value is consistent when used with proxies, we // update the local fallback_value, but only if the fallback is actively used @@ -386,10 +382,10 @@ export function prop(props, key, flags, fallback) { } // TODO is this still necessary post-#16263? - if (has_destroyed_component_ctx(current_value)) { - return current_value.v; + if (has_destroyed_component_ctx(d)) { + return d.v; } - return get(current_value); + return get(d); }; } From c9cf7e3044a01545acb603e2c42d00441a67ccd2 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 30 Jun 2025 22:10:12 -0400 Subject: [PATCH 09/13] tweak --- packages/svelte/src/internal/client/reactivity/props.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js index 9420b8e94fef..de64bd7806e1 100644 --- a/packages/svelte/src/internal/client/reactivity/props.js +++ b/packages/svelte/src/internal/client/reactivity/props.js @@ -259,7 +259,6 @@ function has_destroyed_component_ctx(current_value) { * @returns {(() => V | ((arg: V) => V) | ((arg: V, mutation: boolean) => V))} */ export function prop(props, key, flags, fallback) { - var immutable = (flags & PROPS_IS_IMMUTABLE) !== 0; var runes = !legacy_mode_flag || (flags & PROPS_IS_RUNES) !== 0; var bindable = (flags & PROPS_IS_BINDABLE) !== 0; var lazy = (flags & PROPS_IS_LAZY_INITIAL) !== 0; @@ -361,7 +360,7 @@ export function prop(props, key, flags, fallback) { // prop is written to, but there's no binding, which means we // create a derived that we can write to locally - var d = (immutable ? derived : derived_safe_equal)(getter); + var d = ((flags & PROPS_IS_IMMUTABLE) !== 0 ? derived : derived_safe_equal)(getter); // Capture the initial value if it's bindable if (bindable) get(d); From 73d66a8dbf83d45a617251e1fa599ace329b8ce2 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 30 Jun 2025 22:14:20 -0400 Subject: [PATCH 10/13] also appears to be unnecessary --- packages/svelte/src/internal/client/reactivity/props.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js index de64bd7806e1..a7f9daf34c2c 100644 --- a/packages/svelte/src/internal/client/reactivity/props.js +++ b/packages/svelte/src/internal/client/reactivity/props.js @@ -265,11 +265,8 @@ export function prop(props, key, flags, fallback) { var fallback_value = /** @type {V} */ (fallback); var fallback_dirty = true; - var fallback_used = false; var get_fallback = () => { - fallback_used = true; - if (fallback_dirty) { fallback_dirty = false; @@ -320,7 +317,6 @@ export function prop(props, key, flags, fallback) { var value = /** @type {V} */ (props[key]); if (value === undefined) return get_fallback(); fallback_dirty = true; - fallback_used = false; return value; }; } else { @@ -371,9 +367,7 @@ export function prop(props, key, flags, fallback) { set(d, new_value); - // To ensure the fallback value is consistent when used with proxies, we - // update the local fallback_value, but only if the fallback is actively used - if (fallback_used && fallback_value !== undefined) { + if (fallback_value !== undefined) { fallback_value = new_value; } From dfbce4dd61aa86c556977a2e27c25a310d9b9fca Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 30 Jun 2025 22:16:12 -0400 Subject: [PATCH 11/13] changeset --- .changeset/new-trees-behave.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/new-trees-behave.md diff --git a/.changeset/new-trees-behave.md b/.changeset/new-trees-behave.md new file mode 100644 index 000000000000..d5fab30f3ea2 --- /dev/null +++ b/.changeset/new-trees-behave.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +chore: simplify props From 393332805e906728b5342ddf89e0f36e3550ce50 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 1 Jul 2025 13:35:01 -0400 Subject: [PATCH 12/13] apparently this is also unnecessary --- packages/svelte/src/internal/client/reactivity/props.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js index a7f9daf34c2c..21c092316d09 100644 --- a/packages/svelte/src/internal/client/reactivity/props.js +++ b/packages/svelte/src/internal/client/reactivity/props.js @@ -328,7 +328,7 @@ export function prop(props, key, flags, fallback) { } // prop is never written to — we only need a getter - if (runes && (flags & PROPS_IS_UPDATED) === 0) { + if ((flags & PROPS_IS_UPDATED) === 0) { return getter; } From 0ef587aa6a239dd5e5f6345eaa8c7a163dd37ebc Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Tue, 1 Jul 2025 13:57:54 -0400 Subject: [PATCH 13/13] explanatory comment --- .../svelte/src/internal/client/reactivity/props.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/svelte/src/internal/client/reactivity/props.js b/packages/svelte/src/internal/client/reactivity/props.js index 21c092316d09..f51291b1cc0c 100644 --- a/packages/svelte/src/internal/client/reactivity/props.js +++ b/packages/svelte/src/internal/client/reactivity/props.js @@ -322,7 +322,15 @@ export function prop(props, key, flags, fallback) { } else { getter = () => { var value = /** @type {V} */ (props[key]); - if (value !== undefined) fallback_value = /** @type {V} */ (undefined); + + if (value !== undefined) { + // in legacy mode, we don't revert to the fallback value + // if the prop goes from defined to undefined. The easiest + // way to model this is to make the fallback undefined + // as soon as the prop has a value + fallback_value = /** @type {V} */ (undefined); + } + return value === undefined ? fallback_value : value; }; }