Skip to content

Snippets using props will error if rendered outside the component after its unmounted #14878

@lassebomh

Description

@lassebomh

Describe the bug

We're trying to upgrade to Svelte 5 at my company, but we ran into this bug when trying to migrate our portals implementation.
In short, the goal with portals is to allow a child component to insert one of its elements into an element of another component via an imported reference, instead of prop drilling. I'll spare you the other details and get right into the issue.

This is the bug:

First we have a file with an exported object containing a snippet:

// portals.svelte.js

export let headerPortal = $state({ snippet: null });

And our App.svelte renders this snippet, as well as containing a dynamic component:

<script>
  import { headerPortal } from './portals.svelte'

  let route = { component: ..., props: ... }
</script>

{#if headerPortal.snippet !== null}
  <header>
    {@render headerPortal.snippet()}
  </header>
{/if}

<route.component {...route.props} />

Then we have a component that sets headerPortal.snippet. The snippet has to contain a reference to a prop (2-levels deep):

<script>
  import { headerPortal } from "./portals.svelte";

  let { data } = $props();

  $effect.pre(() => {
    headerPortal.snippet = mySnippet;

    return () => {
      headerPortal.snippet = null;
    };
  });
</script>

{#snippet mySnippet()}
  <h1>{data.text}</h1>
{/snippet}

If we use our dynamic component in App.svelte to mount this one, and then unmount it, then we will get an error:

Uncaught TypeError: Cannot read properties of undefined (reading 'text')

Reproduction

Bug REPL

Open the console and click "Show page 2" and then "Show page 1". You should see an error, similar to the one above.

https://svelte.dev/playground/1175b32356ca41d4953c2896f71871ac?version=5.16.0

I haven't been able to reduce it any further.

Workaround REPL

https://svelte.dev/playground/c214d0b1cb5c4e6cba00c85a3704391f?version=5.16.0

It makes a change to the router:

<script>
  let route = ...
  let someKey = $state(0) // increment whenever route changes
</script>

{#if route !== null}
	{#each [route] as route (someKey)}
		<route.component {...route.props} />
	{/each}
{/if}

It's a complete hack. I expected {#key someKey } to work the same way, but it doesn't. It has to be a keyed each block

Logs

No response

System Info

System:
    OS: Windows 11 10.0.26100
    CPU: (16) x64 AMD Ryzen 9 6900HS with Radeon Graphics        
    Memory: 16.66 GB / 31.25 GB
  Binaries:
    Node: 20.12.2 - C:\Program Files\nodejs\node.EXE
    npm: 10.5.2 - C:\Program Files\nodejs\npm.CMD
    pnpm: 9.12.3 - C:\Program Files\nodejs\pnpm.CMD
  Browsers:
    Edge: Chromium (131.0.2903.112)
    Internet Explorer: 11.0.26100.1882

Severity

annoyance

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions