Skip to content

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

Closed
@kepzAT

Description

@kepzAT

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

Activity

kepzAT

kepzAT commented on May 6, 2025

@kepzAT
Author

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

brunnerh commented on May 6, 2025

@brunnerh
Member

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

kepzAT commented on May 6, 2025

@kepzAT
Author

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

brunnerh commented on May 6, 2025

@brunnerh
Member

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

7nik commented on May 6, 2025

@7nik
Contributor

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

brunnerh

brunnerh commented on May 6, 2025

@brunnerh
Member

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.

raythurnvoid

raythurnvoid commented on Jun 2, 2025

@raythurnvoid
Contributor
added theissue type on Jun 10, 2025
added this to the 5.x milestone on Jun 10, 2025
dummdidumm

dummdidumm commented on Jun 10, 2025

@dummdidumm
Member

Confirmed that this is due to #15553, the wrong effect is set as active so updates are not notified.

Update: The problem is that reaction_sources is pushed to, and then in get it says "oh there's the same signal in reaction_sources so I'm gonna skip it", even though we're not executed in the context of the reaction the signal was created in. So the check needs to be enhanced to see if we're inside the same reaction.

added a commit that references this issue on Jun 17, 2025
cb28da5

2 remaining items

Loading
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    Projects

    No projects

    Relationships

    None yet

      Participants

      @brunnerh@dummdidumm@Conduitry@7nik@raythurnvoid

      Issue actions

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