I have a component that looks like this:
<script lang="ts">
import Component2 from './Component2.svelte'
const {value, setState} = $props()
const closure = () => console.log('value is undefined:', value)
const next = () => setState({component: Component2, props: {closure}})
$inspect(value)
</script>
<button onclick={next}>Next</button>
The setState
function changes state in the parent component, which then renders the passed component (Component2
in this case) with the given props. Component2 then calls the closure
function.
The console output for this program (on a development build) is:
init 1
update undefined
value is undefined: undefined
What's happening is value
is getting set to undefined when Component1 unmounts. value
is closed over by the closure
function, which means it is undefined when Component2 later calls it.
This change fixes the problem:
const valueCopy = value
const closure = () => console.log('value is not undefined:', copyValue)
This clarifies that the problem is not that value
is a proxy with deep reactivity, but is being reassigned because it's a prop of Component
.
The problem is that this behavior is new with svelte5, and didn't used to be the case in svelte4. This breaks a bunch of stuff with my (real) application. It also seems like very annoying behavior, since now I have to explicitly re-assign every variable I don't want to be destroyed, in every component where this (fairly complex) pattern exists.
Is there any other (better) way to handle this?
For completeness, my use case is controlling modal state. Here's an example from my real-world application:
const onEmoji = (emoji: NativeEmoji) =>
publishReaction({event: e, relays: [url], content: emoji.unicode})
const showEmojiPicker = () =>
pushModal(EmojiPicker, {onClick: onEmoji})
Minimal reproduction:
- Source code:
- Demo: /
I have a component that looks like this:
<script lang="ts">
import Component2 from './Component2.svelte'
const {value, setState} = $props()
const closure = () => console.log('value is undefined:', value)
const next = () => setState({component: Component2, props: {closure}})
$inspect(value)
</script>
<button onclick={next}>Next</button>
The setState
function changes state in the parent component, which then renders the passed component (Component2
in this case) with the given props. Component2 then calls the closure
function.
The console output for this program (on a development build) is:
init 1
update undefined
value is undefined: undefined
What's happening is value
is getting set to undefined when Component1 unmounts. value
is closed over by the closure
function, which means it is undefined when Component2 later calls it.
This change fixes the problem:
const valueCopy = value
const closure = () => console.log('value is not undefined:', copyValue)
This clarifies that the problem is not that value
is a proxy with deep reactivity, but is being reassigned because it's a prop of Component
.
The problem is that this behavior is new with svelte5, and didn't used to be the case in svelte4. This breaks a bunch of stuff with my (real) application. It also seems like very annoying behavior, since now I have to explicitly re-assign every variable I don't want to be destroyed, in every component where this (fairly complex) pattern exists.
Is there any other (better) way to handle this?
For completeness, my use case is controlling modal state. Here's an example from my real-world application:
const onEmoji = (emoji: NativeEmoji) =>
publishReaction({event: e, relays: [url], content: emoji.unicode})
const showEmojiPicker = () =>
pushModal(EmojiPicker, {onClick: onEmoji})
Minimal reproduction:
- Source code: https://github/staab/svelte5-closure-bug
- Demo: https://svelte5-closure-bug.onrender/
1 Answer
Reset to default 0I filed a bug report with the svelte team: https://github/sveltejs/svelte/issues/15325
The response was that this behavior is by design. Props "belong" to the parent component, and are passed as getters, which means they can be revoked at any time. I don't understand why this is necessary, but I'll take for granted it is.
pushModal(MyModal, {callback})
. MyModal is generic, and it's very idiomatic to pass functions as values in javascript. I don't like that svelte breaks this pattern. – jstaab Commented Feb 17 at 23:39Parent
passescallback
toComponentX
as a prop, thenComponentX
triggerspushModal()
in a fire-and-fet mode, unloading itself? Am I understanding? – José Ramírez Commented Feb 17 at 23:47PushModal(B, {callback})
, B calls the callback. By that time, A is unmounted and any props of A that were closed over bycallback
have been set toundefined
– jstaab Commented Feb 18 at 1:11