Search code examples
sveltesveltekitsvelte-5

Imported Svelte State not updating


I am running into a error with reactive state in Svelte 5 with SvelteKit.

I've written a simple helper for syncing a state with remote data.

// $lib/remoteProxy.svelte.js
export const remoteProxy = (val, fetcher) => {
    let proxy = $state(val);
    let loading = $state(false);
    const mutate = async (f) => {
        proxy = f(proxy)
        loading = true
        proxy = await fetcher()
        loading = false

    }
    return { proxy, mutate, loading }
}

But when I import this into a page, this state is no longer reactive.

<script>
  import { remoteProxy } from '$lib/remoteProxy.svelte.js';
  // sample response would be { records: RecordObject[], limit: 10, page: 1 }
  const remotePaginatedRecords = remoteProxy([], () => {
    fetch('https://paginated.api/');
  });
  const recordResponse = $derived(remotePaginatedRecords.proxy);
  const mutateRecords = remotePaginatedRecords.mutate;
  const handleCreateRecord = () => {
    mutateRecords((currentRecords) => {
      currentRecords.records.push({});
    });
  };
</script>
{#each recordResponse.records as record}
  <Record {record} />
{/each}
<button onclick={handleCreateRecord}>Create</button>

In this case, updating (appending, deleting, renaming, updating values) via mutate does not rerender the each clause.

On mutating the state, $inspect reveals the remoteProxy proxy state being updated, but inspecting the recordResponse shows that it is only set initially, and not updated reactively when proxy changes. I've tried variants of each object being $derived, but nothing has worked so far.

The documents indicate that the state proxies in Svelte 5 are deep. Shouldn't this be forcing a new render?


Solution

  • This cannot work because you replace the reference.

    The return { proxy, ... } returns the initial proxy object but in the mutate function the reference is set to a new object. Since the return has already happened, nothing else will have the new reference.

    This is why generally one should return objects that close over the variable, thus retaining a live reference, either via a function or a get/set property.

    E.g. in this case the return could use getters:

    return {
      get proxy() { return proxy; },
      mutate,
      get loading() { return loading; },
    }
    

    (By the way, the proxy = f(proxy) does not seem to fit the usage, the example mutation function does not return anything.)