I'm writing my first app in Svelte today, and I've come across a basic logic issue when trying to create a non-trivial system of stores.
As the question title says, I am struggling with finding a way to orchestrate store updates in which one store is updated in a certain way, dependant upon the current values of another store.
The docs state that its possible to get the state value of a store by using get
like so:
import { get } from 'svelte/store';
const value = get(store);
But the docs also state:
Generally, you should read the value of a store by subscribing to it and using the value as it changes over time. Occasionally, you may need to retrieve the value of a store to which you're not subscribed. get allows you to do so.
This works by creating a subscription, reading the value, then unsubscribing. It's therefore not recommended in hot code paths.
This makes me wonder if I should avoid this pattern of usage where I grab the state value of the store using "get" without a real subscription, but I am not sure how else I can coordinate updates to multiple stores.
To help explain, I've written a few examples of ways that I think I could do this, but all of these examples don't seem ideal.
In my example, I'm using an app where you have to shoot a target, but the targets HP goes down by the amount of the first type of ammo that is in your revolver's chamber.
The app begins with an input listener.
#############################################################
# Step 1: Create an input listener
#############################################################
const keyboardListener = (() => {
const subscribers = new Map();
document.addEventListener('keyup', event => {
if (event.code === 'Space') {
[...subscribers].forEach(sub => sub({ eventType: 'fire' }));
}
});
function subscribe(fn) {
this.subscribers.set(fn, fn);
return () => {
this.subscribes.delete(fn);
};
}
return {
subscribe,
};
})();
Now lets try to create a store...
#############################################################
# Step 2a: Store architecture pattern 1: The problem
#############################################################
export const target = writable({
name: 'cow',
hp: 100,
});
export const ammo = writable([
{
type: 'armourPiercing',
damage: 20,
},
{
type: 'explosive',
damage: 50,
},
{
type: 'flash',
damage: 1,
}
]);
keyboardListener.subscribe(() => {
// If we have ammo, get the first ammo and reduce the cows HP.
target.set(currentTargetValue => ({
...currentTargetValue,
hp: currentTargetValue.hp - ammoDamage // Where does ammoDamage come from?
}));
});
The problem with the store above, is that I'm not sure how to get the value of the ammo store while in the update callback function for the target store.
I've realised that I can overcome this by just having the app store as one, big, monolitic thing. Then I can always get all of its state when doing an update.
#############################################################
# Step 2b: Store architecture pattern 2: One big store
# (So we can access the current value of both stores during
# the update)
#############################################################
export const wholeAppStore = writable({
target: {
name: 'cow',
hp: 100,
},
ammo: [
{
type: 'armourPiercing',
damage: 20,
},
{
type: 'explosive',
damage: 50,
},
{
type: 'flash',
damage: 1,
}
]
});
keyboardListener.subscribe(() => {
// If we have ammo, get the first ammo and reduce the cows HP.
wholeAppStore.set(currentWholeAppStoreValue => {
const newAmmo = JSON.parse(JSON.stringify(currentWholeAppStoreValue.ammo));
const firstAmmo = newAmmo.pop();
const newTarget = JSON.parse(JSON.stringify(currentWholeAppStoreValue.target));
newTarget.hp -= firstAmmo.damage;
return {
target: newTarget,
ammo: newAmmo,
};
});
});
But having one big monolithic app store doesn't seem sensible. Its awkward to write updates for, and I wonder if it is probably not great performance-wise either.
So lets try using the get
function.
#############################################################
# Step 2c: Store architecture pattern 3: Use "get"?
#############################################################
export const target = writable({
name: 'cow',
hp: 100,
});
export const ammo = writable([
{
type: 'armourPiercing',
damage: 20,
},
{
type: 'explosive',
damage: 50,
},
{
type: 'flash',
damage: 1,
}
]);
keyboardListener.subscribe(() => {
const ammoValue = get(ammo);
const firstAmmo = [...ammoValue].pop();
const ammoDamage = firstAmmo.damage;
// If we have ammo, get the first ammo and reduce the cows HP.
target.set(currentTargetValue => ({
...currentTargetValue,
hp: currentTargetValue.hp - ammoDamage;
}));
});
But I am unsure about this approach due to the messaging in the documentation. Plus, as I have only used get
to pluck the value, the ammo store has not been updated to remove the ammo from the revolvers chamber. Maybe we should just do it with two separate update callbacks.
#############################################################
# Step 2d: Store architecture pattern 4:
# Use messy global vars?
#############################################################
export const target = writable({
name: 'cow',
hp: 100,
});
export const ammo = writable([
{
type: 'armourPiercing',
damage: 20,
},
{
type: 'explosive',
damage: 50,
},
{
type: 'flash',
damage: 1,
}
]);
keyboardListener.subscribe(() => {
let firstAmmo; // "Global" var
ammo.set(currentAmmoValue => {
const newAmmo = JSON.parse(JSON.stringify(currentAmmoValue));
firstAmmo = newAmmo.pop();
return newAmmo;
});
const ammoDamage = firstAmmo.damage;
// If we have ammo, get the first ammo and reduce the cows HP.
target.set(currentTargetValue => ({
...currentTargetValue,
hp: currentTargetValue.hp - ammoDamage;
}));
});
This works I think, but it feels hacky.
So you see, I am having trouble finding a good pattern for a larger app store architecture. And of course, this is actually a relatively simple app - a real world app would of course be much more complex.
So I am wondering - are there any established patterns out there for managing large, inter-related stores in Svelte?