Description
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.
LoadingstateDiagram-v2 direction RL # classDef Readable font-weight:bold,stroke-width:3 # class C Readable B --> A C --> A C --> BSvelte'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 inderived()
. Immediately afterwards, both stores B and C re-evaluate, store C using the old value of store B that it recorded in its privatevalues
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 aTypeError
.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
NaN
s andundefined
s 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
.