8000 Derived stores are re-evaluated on invalid upstream state · Issue #10376 · sveltejs/svelte · GitHub
[go: up one dir, main page]

Skip to content
Derived stores are re-evaluated on invalid upstream state #10376
Open
@mnrx

Description

@mnrx

Describe the bug

Note: The following block quote is from #9458.

Currently, a change to a store's value only causes stores which are immediately dependent on it to be invalidated. Svelte does not propagate invalidation downstream to subsequent derived stores, and this can cause issues when two criteria are met:

  • the store dependency graph forks and merges again at least once; and
  • the two or more paths between the fork and merge points are of unequal length.

Svelte's current implementation correctly handles dependency diamonds, but such cases do not meet the second criterion; unequal path length.

Example

Consider the following dependency graph—A is a readable store, while B and C are derived stores.

stateDiagram-v2
  direction RL

  # classDef Readable font-weight:bold,stroke-width:3
  # class C Readable
  
  B --> A
  C --> A
  C --> B
Loading

Svelte's Current Implementation

In Svelte's current implementation, when the value of store A changes, stores B and C are each invalidated once. This is recorded in the pending variable in derived(). Immediately afterwards, both stores B and C re-evaluate, store C using the old value of store B that it recorded in its private values array during initialisation of the store graph.

Once stores B and C have changed value, they invalidate their subscribers. Store C has no subscribers to invalidate, but is invalidated itself by its dependency, store B. This causes store C to re-evaluate a second time, this time reaching the correct value.

The first re-evaluation of store C is wasted. This is arguably out of keeping with Svelte's efficiency, but is also—and more importantly—performed on invalid state. An exception could be thrown if, for instance:

  • Store A held an array of objects.
  • Store B held an index within that array.
  • Store C held a property of the element of the array at that index.
  • The array stored in store A were to shorten below the index in store B.

In this case, the index (store B) will become out of range, the object will become undefined, and property access on that value will yield a TypeError.

I suspect that JavaScript's loose typing allows a lot of these premature re-evaluations to go unnoticed in many real-world applications, generating short-lived NaNs and undefineds rather than throwing errors. I did encounter this issue organically, though.

See my PR (#9458) for a different implementation of svelte/store which avoids this issue. My implementation treats derived stores, in their capacity as a store subscriber themselves, with extra attention. This enables more accurate tracking of derived store validity and ensures that re-evaluation doesn't happen on invalid upstream state.

Reproduction

The following example has sound application logic. If the two derived stores were combined into one, it would work as expected. Instead, Svelte re-evaluates the assertion store twice for every change in the writable store number. The first of each of these re-evaluations feeds invalid state to assertion's update function, producing an invalid output, e.g., "3 × 3 is even".

<!-- App.svelte -->
<script>
  import { onDestroy } from 'svelte';
  import { writable, derived } from 'svelte/store';
  
  const values = [];

  let number = writable(1);
  const triple = derived(number, ($number) => $number * 3);
  const assertion = derived(
    [number, triple],
    ([$number, $triple]) => `${$number} × 3 is ${$triple % 2 === 0 ? 'even' : 'odd'}`,
  );

  let assertions = [];
  const unsubscribe = assertion.subscribe(a => assertions = [...assertions, a]);
  
  onDestroy(unsubscribe);
</script>

Increment the field value and observe the messages.<br>
<input type="number" bind:value={$number}>
<pre>{assertions.join('\n')}</pre>

Logs

Regarding logs, no exceptions are thrown unless the invalid state causes the derived store's update function to throw one. I encountered this issue while writing a derived store which attempted to access a property of an object in an array, with both the array and index being the values of other stores. In this case, the error would show "Uncaught TypeError: can't access property "foo" of undefined."

System Info

n/a

Severity

Blocking all use of svelte/store.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions

      0