I'm making a swimming app and I want a button that adds an event to a swim meet. In place of a store, I'm using an object called STATE that acts as a single source of truth for the entire app.
<script>
import Event from '@shared/models/Event';
import { STATE, addEventToMeet } from '@src/state/state.svelte.js';
import EventEditor from './EventEditor.svelte';
let meet = $state(STATE.meet);
let events = $derived(STATE.meet.events);
let lastEvent = events[events.length - 1];
let name = $derived(STATE.meet.name);
function addEvent(eventData=lastEvent) {
console.log("Adding event", eventData);
let newEvent = new Event({
...eventData,
n: (eventData?.n || 0) + 1,
});
addEventToMeet(newEvent);
events = [...events, newEvent]; // Why is this necessary?
console.log(STATE.meet.events, events);
}
</script>
<div>
{name}
<div class = 'events'>
{#each events as event (event.key)}
<EventEditor {event} />
{/each}
<button class = 'sb tool new-event'
onclick={() => addEvent()}>+ New Event
</button>
</div>
</div>
For some reason, I can only get the list of events to update reactively with the line events = [...events, newEvent]. But I thought the whole point of Svelte 5 deep reactivity was that I could just reassign STATE.meet.events and it would automatically update. For reference, here is addEventToMeet
export function addEventToMeet(newEvent) {
STATE.meet.events = [...STATE.meet.events, newEvent];
}
I tried the above without reassigning events, and what happens is that both STATE.meet.events is updated, but events is not updated unless I reload the component. I was expecting events to update reactively since it is $derived from STATE.meet.events.
////////////////////// EDIT ////////////////////// I have isolated the issue to 2 files:
state.svelte.js
import Meet from "@src/shared/models/Meet";
export const TEST1 = $state({
meet: new Meet({name: 'Test Meet', meetType: ''}),
});
export const TEST2 = $state({
meet: {
name: "test meet",
type: {
eventsTemplate: {
events: [
{ n: 1,
stroke: "Free",
distance: 100
}
]
}
}
}
});
Test component:
<script>
import { TEST1, TEST2 } from '@src/state/state.svelte.js';
import Meet from '@shared/models/Meet';
if (!TEST1.meet) TEST1.meet = new Meet({name: 'Test Meet 1', meetType: ''});
console.log("TEST1", TEST1.meet);
let events1 = $derived(TEST1.meet.type.eventsTemplate.events);
let events2 = $derived(TEST2.meet.type.eventsTemplate.events);
function addEvent() {
const newEvent = {
n: events2.length + 1,
distance: 100,
stroke: 'Freestyle'
};
TEST1.meet.type.eventsTemplate.events.push(newEvent);
TEST2.meet.type.eventsTemplate.events.push(newEvent);
console.log("Events after adding", TEST1.meet.type.eventsTemplate.events);
}
</script>
<div>
{TEST2.meet.name}
<button class='sb tool new-event'
onclick={() => addEvent()}>+ New Event
</button>
<h3>EVENTS 1</h3>
<div class = 'events'>
{#each events1 as event, index}
<div>
Event {event.n} - {event.distance} {event.stroke}
</div>
{/each}
</div>
<h3>EVENTS 2</h3>
<div class = 'events'>
{#each events2 as event, index}
<div>
Event {event.n} - {event.distance} {event.stroke}
</div>
{/each}
</div>
</div>
What happens here is that EVENTS 2 is reactive and updates when I click the Add Event button, but to get EVENTS 1 to update, I have to re-render the component.
So it seems that creating a new Meet object and assigning it to the TEST1 state ruins reactivity. However, I would prefer for STATE.meet to be a full Meet object with all its methods rather than a basic JS object as in TEST2.
STATE, it's probably wrong. You also don't need to spread in Svelte 5 and can justpushto the array, if$stateis used correctly. Themeetvariable looks like a mistake as well, it creates a detached new state.STATEis aletdeclaration is also a red flag, if it is reassigned, all locations that import the object will hold the wrong reference.