I have a data model that I can't change in this project. I'm trying to strip down and simplify the sample code below, so hopefully this still makes sense with what I'm trying to re-produce.
Let's say I have two stores. One store holds "containers" and the other store holds "items" - each store is independently used throughout the app for various purposes. The Container only holds Item IDs by default. Occasionally, I want to de-normalize the data for use on some pages. I use a derived store and denormalize each of the container objects, turning them into DenormalizedContainers.
Let's say I have a use case where I want to create a new Item object, and then add it to a given Container. As far as I can tell, that will cause the derived store to update twice (once for the change to items and again for a change to containers), and thus anything that uses that derived store will update twice (depending on how it's called).
Why would the behaviour be different from subscribe to $?
Also, while this isn't a huge problem for me, I'm just curious if there is any native Svelte-y way to workaround this without changing the data model (while still being able to use the subscribe API?
<script lang="ts">
import { derived, writable } from "svelte/store";
type Container = {
id: string;
itemIds: string[];
};
type Item = {
id: string;
name: string;
};
type DenormalizedContainer = {
id: string;
items: Item[];
};
const containers = writable<Container[]>([]);
const items = writable<Item[]>([]);
const denormalizedContainers = derived(
[containers, items],
([$containers, $items]): DenormalizedContainer[] => {
return $containers.map(({ id, itemIds }) => {
const denormalizedContainer = {
id,
items: itemIds.map((id) =>
$items.find((item) => {
item.id === id;
})
),
};
return denormalizedContainer;
});
}
);
function handleAddContainer() {
const value = Math.random() * 1000;
const newContainer: Container = { id: `new-container-${value}`, itemIds: [] };
containers.set([...$containers, newContainer]);
}
function handleAddItem() {
const value = Math.random() * 1000;
const newItem: Item = { id: `new-id-${value}`, name: `new-name-${value}` };
items.set([...$items, newItem]);
}
function handleAddBoth() {
const value = Math.random() * 1000;
const newItem: Item = { id: `new-id-${value}`, name: `new-name-${value}` };
const newContainer: Container = { id: `new-container-${value}`, itemIds: [newItem.id] };
items.set([...$items, newItem]);
containers.set([...$containers, newContainer]);
}
$: console.log(`$: There are ${$containers.length} containers`);
$: console.log(`$: There are ${$items.length} items`);
$: console.log(`$: There are ${$denormalizedContainers.length} denormalized containers`);
denormalizedContainers.subscribe((newValue) =>
console.log(
`Subscribe: There are ${$denormalizedContainers.length} denormalized containers`
)
);
</script>
<button class="block" on:click={() => handleAddContainer()}>Add container</button>
<button class="block" on:click={() => handleAddItem()}>Add item</button>
<button class="block" on:click={() => handleAddBoth()}>Add container and item</button>
Results:
Upon clicking each button once, here are the logs:
Page load:
Subscribe: There are 0 denormalized containers
$: There are 0 containers
$: There are 0 items
$: There are 0 denormalized containers
Add container:
Subscribe: There are 1 denormalized containers
$: There are 1 containers
$: There are 1 denormalized containers
Add item:
Subscribe: There are 1 denormalized containers
$: There are 1 items
$: There are 1 denormalized containers
Add both:
Subscribe: There are 1 denormalized containers
Subscribe: There are 2 denormalized containers
$: There are 2 containers
$: There are 2 items
$: There are 2 denormalized containers
The updates handled via $ are what I would like all the time, but for some reason the manual subscribe API shows two updates. I have a couple locations outside of .svelte files, where I would need to use the subscribe API.
References:
Similar to this, but doesn't explain why the $ behaves differently to the subscribe.
Svelte Derived Store atomic / debounced updates
Possibly the underlying problem?
https://github.com/sveltejs/svelte/issues/6730
Possibly explained in 3.1/3.2 here. Micro-tasks bundle up reactive statements and apply them at the end of the tick. Whereas subscribe handles updates in real-time. Note: Wrapping my subscribe code in a tick just makes it run twice with the same data.