Skip to content
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

Fetching an object from a SveltMap, modifying it, then setting it back does not have obvious behavior #14386

Open
ibuildthecloud opened this issue Nov 21, 2024 · 5 comments

Comments

@ibuildthecloud
Copy link

Describe the bug

For $state([]) you can get an object by index, change it and then set it back and this reactively change. My assumption was that the same behavior would work for a $state(new SvelteMap()) which does not seem to be the case. If you get a value that is an object, modify it and then set it back no reactivity seems to happen. If I set a new object with the same key, things to reactively change. Refer to the playground link for an example.

Reproduction

https://svelte.dev/playground/a1144052a70346f9bced094f4d795823?version=5.2.7

Logs

No response

System Info

System:
    OS: Linux 5.15 Ubuntu 24.04.1 LTS 24.04.1 LTS (Noble Numbat)
    CPU: (12) arm64 unknown
    Memory: 17.04 GB / 31.17 GB
    Container: Yes
    Shell: 5.2.21 - /bin/bash
  Binaries:
    Node: 18.19.1 - /usr/bin/node
    npm: 9.2.0 - /usr/bin/npm
    pnpm: 9.12.3 - ~/.local/share/pnpm/pnpm
  Browsers:
    Chromium: 130.0.6723.116
  npmPackages:
    svelte: ^5.2.7 => 5.2.7

Severity

annoyance

@brunnerh
Copy link
Member

I would say this is expected; possibly a documentation issue if this is not explained.

The values are not proxied by default, so changing their properties does nothing.
Setting the same key to the same object does nothing and can be omitted in the array case as well.

If you make the object a $state, you can modify its properties and changes will be tracked.

<script>
	import { SvelteMap } from 'svelte/reactivity';
 	let map = $state(new SvelteMap())
	setTest('unset');

	function setTest(value) {
		const test = $state({ value });
		map.set('test', test);
	}
</script>

<h1>Map Value: {map.get('test').value}</h1>

<button onclick={() => setTest('replaced')}>
	Replace value
</button>
<button onclick={() => map.get('test').value = 'modifiedobj'}>
	Change value
</button>

Playground

@Leonidaz
Copy link

also, SvelteMap() is already reactive and doesn't need to be wrapped in $state()

@harrisi
Copy link

harrisi commented Nov 21, 2024

also, SvelteMap() is already reactive and doesn't need to be wrapped in $state()

I actually kind of think something somewhere should warn about this.

@ibuildthecloud
Copy link
Author

also, SvelteMap() is already reactive and doesn't need to be wrapped in $state()

This would be a great thing to document. I thought the documentation where it talks about svelte providing reactive utilities meant that $state(new Map()) wouldn't work but $state(new SvelteMap()) would.

Setting the same key to the same object does nothing and can be omitted in the array case as well.

Maybe this a dumb question. I don't think I understand what a reactive map actually means. So if you do let x = map.get('x') and later something sets x, it sounds like that does nothing. So what is reactive about a map? Is it just traversing keys is reactive?

@brunnerh
Copy link
Member

brunnerh commented Nov 21, 2024

Side note: $state(new SvelteMap()) is still required if the entire object is reassigned rather than modified.

So what is reactive about a map?

The difference is that any change to the object via the map's methods trigger reactivity.
If the code that uses the methods is not in a reactive context, there is functionally no difference to a regular map.

Usage in the template, an $effect or $derived only behaves correctly with a reactive map like SvelteMap.

Example:

<svelte:options runes />
<script>
	import { SvelteMap } from 'svelte/reactivity';
	import MapOutput from './MapOutput.svelte';

	let map = new Map();
	let svelteMap = new SvelteMap();

	function addValues() {
		map.set(map.size, new Date().toISOString());
		svelteMap.set(svelteMap.size, new Date().toISOString());
	}

	$effect(() => {
		// only triggers on mount
		console.log('Normal map', [...map]);
	});
	$effect(() => {
		console.log('Svelte map', [...svelteMap]);
	});
</script>

<button onclick={addValues}>Add values</button>

<MapOutput map={map}>Normal map</MapOutput>
<MapOutput map={svelteMap}>Svelte map</MapOutput>
<!-- MapOutput.svelte -->
<script>
	const { map, children } = $props();
</script>

<div>
	<strong>{@render children()}</strong> <br>
	Size: {map.size} <br>
	<pre>{JSON.stringify([...map], null, 2)}</pre>
</div>

(Output is split off, otherwise the DOM might still update due to render effect batching, i.e. SvelteMap triggers a DOM update and the part showing the regular Map is also forced to update, making it look reactive when it actually isn't.)

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

No branches or pull requests

5 participants