Skip to content

Components added to dom using 'mount' have broken reactivity in {#if} when reacting to state from context #15870

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

Open
kepzAT opened this issue May 6, 2025 · 6 comments
Labels

Comments

@kepzAT
Copy link

kepzAT commented May 6, 2025

Describe the bug

If you mount a component to the DOM programmatically using mount, and pass in a $state object via context. reactivity to the state with {#if} statements does not work correctly.
Playground:
https://svelte.dev/playground/63a37aca753544ba9c2f96dd5bd4cd3a?version=5.28.2

In this snippet I show something like:

{#if value === true}
    {value}
{/if}

but the rendered HTML shows both "true" and "false".

Some more observations

  • Duplicating this if block in the template twice makes it work
  • There are problems with $derived not reacting to changing state as well

Reproduction

App.svelte

<script>
	import { setContext, getContext, onMount } from 'svelte';
	import { contextTest } from './service.svelte.js';
	
	const stateObjectFromContext = getContext('stateContext')
	let element = undefined

	onMount(() => {
		contextTest(element)
	})
</script>

<div bind:this={element}></div>

service.svelte.js

import { setContext, getContext, mount } from 'svelte';
import NestedComponent from './NestedComponent.svelte'

export const contextTest = (target) => {
	const stateObject = $state({
		showText: true
	})
	mount(NestedComponent, {target, context: new Map([['stateContext', stateObject]]), props: {}})
		
        setInterval(() => {
		stateObject.showText = !stateObject.showText
	}, 1000)
}

NestedComponent.svelte

<script>
	import { setContext, getContext } from 'svelte';
	import { ContextTest } from './service.svelte.js';

	const stateObjectFromContext = getContext('stateContext')
</script>
<p>Following text is inside an 'if' statement and should only ever be able to show 'true'</p>

{#if stateObjectFromContext.showText === true}
 <h1>{stateObjectFromContext.showText}</h1>
{/if}

Logs

System Info

System:
    OS: Linux 5.15 Ubuntu 22.04.5 LTS 22.04.5 LTS (Jammy Jellyfish)
    CPU: (20) x64 12th Gen Intel(R) Core(TM) i7-12700H
    Memory: 9.89 GB / 15.49 GB
    Container: Yes
    Shell: 5.1.16 - /bin/bash
  Binaries:
    Node: 22.2.0 - ~/.nvm/versions/node/v22.2.0/bin/node
    Yarn: 1.22.22 - /usr/bin/yarn
    npm: 10.7.0 - ~/.nvm/versions/node/v22.2.0/bin/npm
    pnpm: 10.10.0 - /usr/bin/pnpm
  npmPackages:
    svelte: ^5.28.2 => 5.28.2

Severity

blocking all usage of svelte

@Conduitry Conduitry added the bug label May 6, 2025
@kepzAT
Copy link
Author

kepzAT commented May 6, 2025

I can narrow it down more, in this Playground it doesn't work if you modify the state from inside service.svelte.js but if you move the interval inside NestedComponent.svelte if statement works.

@brunnerh
Copy link
Member

brunnerh commented May 6, 2025

That is not how to set context when mounting components dynamically.

  const stateObject = $state({
  	showText: true
  });
- setContext('stateContext', stateObject);
  mount(NestedComponent, {
  	target,
  	props: {},
+  	context: new Map([['stateContext', stateObject]]),
  });

If you want to pass on existing contexts, use getAllContexts and merge that with new contents. Also note that context functions should only be called during component initialization so if you dynamically mount components later, you often have to get context data a lot earlier and store it somewhere to pass it on.

(By the way, you overwrote the state of the original reproduction, so it is unclear what the original issue looked like. I would generally recommend including code in the issue rather than just linking to the playground.)

@kepzAT
Copy link
Author

kepzAT commented May 6, 2025

That is not how to set context when mounting components dynamically.

const stateObject = $state({
showText: true
});

  • setContext('stateContext', stateObject);
    mount(NestedComponent, {
    target,
    props: {},
  • context: new Map([['stateContext', stateObject]]),
    
    });
    If you want to pass on existing contexts, use getAllContexts and merge that with new contents.

(By the way, you overwrote the state of the original reproduction, so it is unclear what the original issue looked like. I would generally recommend including code in the issue rather than just linking to the playground.)

My bad for accidentally overwriting, reverted to the example!

And here is the code:

App.svelte

<script>
	import { setContext, getContext, onMount } from 'svelte';
	import { contextTest } from './service.svelte.js';
	
	const stateObjectFromContext = getContext('stateContext')
	let element = undefined

	onMount(() => {
		contextTest(element)
	})
</script>

<div bind:this={element}></div>

service.svelte.js

import { setContext, getContext, mount } from 'svelte';
import NestedComponent from './NestedComponent.svelte'

export const contextTest = (target) => {
		const stateObject = $state({
			showText: true
		})
		mount(NestedComponent, {target, context: new Map([['stateContext', stateObject]]), props: {}})
		setInterval(() => {
			stateObject.showText = !stateObject.showText
		}, 1000)
}

NestedComponent.svelte

<script>
	import { setContext, getContext } from 'svelte';
	import { ContextTest } from './service.svelte.js';

	const stateObjectFromContext = getContext('stateContext')
</script>
<p>Following text is inside an 'if' statement and should only ever be able to show 'true'</p>

{#if stateObjectFromContext.showText === true}
 <h1>{stateObjectFromContext.showText}</h1>
{/if}

With your suggestion it still does not work for me unfortunately.

@brunnerh
Copy link
Member

brunnerh commented May 6, 2025

Strange bug. Adding an $inspect(stateObjectFromContext); also seems to fix the #if but the inspect does not track any of the changes to the object (only showing the init entry)...

@7nik
Copy link
Contributor

7nik commented May 6, 2025

Wrapping mount in $effect fixes the issue. And creating the stateObject not during onMount, e.g. in <script>, also fixes.

@brunnerh
Copy link
Member

brunnerh commented May 6, 2025

I don't think adding another $effect should be necessary.

Doing some playground bisection, the issue seems to have been introduced in v.5.24.0

Playground @ 5.23.2
Playground @ 5.24.0

Maybe it has to do with this change:

Seen some other issues referencing that in particular.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

4 participants