In Svelte, when using a writable
store that takes a single primitive value (e.g. a string), when firing set
multiple times for the same value, subscribe
will only be called when the value changes. Demo in Svelte REPL
However, when storing a complex object, every time set
is called, even if the object has the same properties, subscribe will still be fired. Demo in Svelte REPL
I'm running a fairly expensive external API call on updates (handled via subscribe), so I want to limit if the data hasn't changed. How best should I prevent firing set or listening to subscribe if the data is the same as the previous run?
An attempt at a solution would be to keep the previous value inside a closure and compare before calling set and then carefully expose which API methods are available for consumers of the store like this:
import { writable, get } from "svelte/store";
const initialContentState = {
title: "",
body: "",
};
const { subscribe, set } = writable(initialContentState);
const isQuestionEqual = (a,b) => {
return a.title === b.title && a.body === b.body;
}
const initQuestionContentStore = () => {
let prevValue = initialContentState;
return {
subscribe,
reset() {
set(initialContentState)
},
setContent(content) {
if (!isQuestionEqual(content, prevValue)) {
set(content);
prevValue = content;
}
}
}
}
export const questionContentStore = initQuestionContentStore()
However, feels weird to have to keep track of the state within the store which is supposed to be responsible for keeping track of state itself. I could also use get
to fetch the value from the store inside of the setContent
method, but docs suggest against it for perf reasons
Note: This is similar to Why does my Svelte store subscribe() get fired when the value hasn't changed?, but I want a workaround, not a reason.
This approach seems fine to me. You could extract the logic for easier reuse/better encapsulation into a writable
wrapper function that takes an optional equality comparison function.
export function strictWritable(value, equalityComparer, start) {
const { subscribe, set: originalSet } = writable(value, start);
let previous = value;
const set = v => {
if (
equalityComparer == null
? v != previous
: equalityComparer(previous, v) == false
) {
originalSet(v);
previous = v;
}
};
return {
subscribe,
update: cb => set(cb(previous)),
set,
}
}
The performance note on get
is about things like retrieving a store value in a loop. In most common cases it should not really matter.
Side note:
The linked question is about something different, namely that the handler will be called for every new subscription which can happen a lot if multiple components use a given store. That is also how get
works: The subscribe
handler will be invoked immediately.
Stores fire on any set
invocation involving objects to allow DOM updates based on mutations. Otherwise you would have to create new object hierarchies when setting a deeply nested property on a store.
In your example you actually set the value to a new object ({ title, body }
), so if you don't want an update triggered here, some additional equality comparisons are necessary which can be non-trivial and impact performance.
Hence, Svelte only checks equality for primitives and otherwise invalidates the store.