Search code examples
javascriptsveltesvelte-5

How to fix non-reactive nested objects inside a $state([]) in Svelte?


Here's the link to repro (Svelte Playground).

The problem is that when you add a "waypoint" and toggle the checkbox to enable its bearings (thus changing the respective nested object's enabled property), Svelte won't see these changes. I can see that because the effect on the lines 41-44 won't log anything. If I use $inpsect instead, there's also nothing.

let { directions, configuration } = {directions: {waypointsBearings: [], configuration: {bearings: true}}, on: () => {}, configuration: {}}

  // @ts-expect-error It's safe to read the plugin's protected properties here.
  if (!directions.configuration.bearings) {
    console.warn("The Bearings Control is used, but the `bearings` configuration option is not enabled!");
  }

    function addWaypoint() {
        directions.waypointsBearings.push({enabled: false})
        onWaypointsChanged()
    }

  let waypointsBearings: {
    enabled: boolean;
    angle: number;
    degrees: number;
  }[] = $state([]);

  // directions.on("addwaypoint", onWaypointsChanged);
  // directions.on("removewaypoint", onWaypointsChanged);
  // directions.on("movewaypoint", onWaypointsChanged);
  // directions.on("setwaypoints", onWaypointsChanged);

  function onWaypointsChanged() {
    waypointsBearings = directions.waypointsBearings.map((waypointBearing, index) => {
      if (waypointsBearings[index]) return waypointsBearings[index];

      return {
        enabled: configuration.defaultEnabled || !!waypointBearing,
        angle: waypointBearing ? waypointBearing[0] : configuration.angleDefault,
        degrees: waypointBearing
          ? waypointBearing[1]
          : configuration.fixedDegrees
            ? configuration.fixedDegrees
            : configuration.degreesDefault,
      };
    });
  }

  $effect(() => {
    console.log("PIA");
    console.log(waypointsBearings);
  });

  onWaypointsChanged();

(The addWaypoint function is artifical and serves to replace the commented-out directions.on calls. It's here just to replace the real map and its interactivity).

<button onclick={addWaypoint}>Add</button>
<div class="bearings-control {configuration.class}" style="display: {waypointsBearings.length ? 'block' : 'none'}">
  <div class="bearings-control__list">
    {#each waypointsBearings as waypointBearing, i}
      <div
        class="
        bearings-control__list-item
        {waypointBearing.enabled ? 'bearings-control__list-item--enabled' : 'bearings-control__list-item--disabled'}
        "
      >
        <span class="bearings-control__number">{i + 1}. </span>
        <input type="checkbox" bind:checked={waypointBearing.enabled} class="bearings-control__checkbox" />

I assume that this absence of reactivity for the inner objects is just a part of the broader issue (see the context below), but should be enough to start.

Here's some broader context for the problem. I have a working code (Svelte 4) which I'm trying to rewrite to Svelte 5. You can see the working Svelte 4 code here and you can see it in action (see that it actually works fine) here (try clicking the map to add waypoints, so that the "bearings" bar appears).


Solution

  • The changes apply as expected.

    See the warning you get when logging state:

    [svelte] console_log_state Your console.log contained $state proxies. Consider using $inspect(...) or $state.snapshot(...) instead
    https://svelte.dev/e/console_log_state

    Changes to $state do not affect the initial object, they are recorded on top of it via a Proxy. If you want to get the current value of the state, take a $state.snapshot(). If you only want to observe the values during debugging, use $inspect.