T
TyphonJS4mo ago
Wasp

Derived Store - To use or not to use

From #dev-lounge - I'm thinking of rewriting my monolithic class-based stores in Item Piles to something more maintainable - when I create a derived store based on a value in another large store, is that faster/better than just creating multiple stores for the properties of the large store?
1 Reply
TyphonJS (Michael)
So, on a cursory review of src/stores I do think a refactor will help quite a bit. The benefit of changing to propertyStore via storing data in a private object with properties of that object turned into stores will allow direct access to this data internally without the many uses of get() from svelte/store. Other aspects since you are using ESM is just getting you up to speed on JSDoc and typing your code and adding comments for future you! You can get all the same coverage and benefits w/ JSDoc with a little knowhow. Keep in mind too that you can use TS files to define types when JSDoc is more cumbersome and still import / use them in JSDoc / ESM code. Now that you have spent several years doing a little bit of prototyping and experimenting, but are now more narrowly focusing on a handful of modules like Item Piles, Sequencer, etc. improving the code for maintenance is a great idea. ----- One of the most complex / compound stores in TRL is TJSPosition. The following is general pseudocode for the general pattern:
import { writable } from 'svelte/store';

import { propertyStore } from '#runtime/svelte/store/writable-derived';

/**
* This is an example of when `MyCompoundStore` is not a store itself. You can review `TJSPosition` for how to make it
* also a store itself which allows subscriptions for when any of the data changes.
*/
export class MyCompoundStore
{
/**
* @type {MyStoreData}
*/
#data = Object.seal(new MyStoreData());

/**
* @type {({
* baz: (value: string) => void
* })}
*/
#readableSet;

/**
* @type {Readonly<MyStores>}
*/
#stores;

constructor()
{
const dataWritable = writable(this.#data);

const readableBaz = propertyStore(dataWritable, 'baz');

// This allows you to easily update readable stores internally.
this.#readableSet = Object.freeze({
baz: readableBaz.set
});

// Creates all externally accessible stores.
this.#stores = Object.freeze({
// Writable stores.
foo: propertyStore(dataWritable, 'foo'),
bar: propertyStore(dataWritable, 'bar'),

// Readable stores.
baz: Object.freeze({ subscribe: readableBaz.subscribe })
});
}

/**
* @returns {Readonly<MyStores>} All the stores!
*/
get stores()
{
return this.#stores;
}

/**
* @returns {string} Contrived demo of accessing internal data in stores for manipulation.
*/
get foobar()
{
// You can directly access data here instead of using `get()` from `svelte/store`.
return `${this.#data.foo}${this.#data.bar}`;
}

doSomething()
{
// Calculate stuff... Update readable store...
this.#readableSet.baz('A new baz!');
}
}

/**
* You don't need to create a data class and can use a bare object with a type alias / `@typedef`. In more complex projects it's probably a good idea.
*/
export class MyStoreData
{
constructor()
{
/** @type {string} */
this.foo = 'foo!';

/** @type {string} */
this.bar = 'bar!';

/** @type {string} */
this.baz = 'baz!';
}
}

/**
* @typedef {object} MyStores
*
* @property {import('svelte/store').Writable<string>} foo Foo store
*
* @property {import('svelte/store').Writable<string>} bar Bar store
*
* @property {import('svelte/store').Readable<string>} baz Baz store
*/
import { writable } from 'svelte/store';

import { propertyStore } from '#runtime/svelte/store/writable-derived';

/**
* This is an example of when `MyCompoundStore` is not a store itself. You can review `TJSPosition` for how to make it
* also a store itself which allows subscriptions for when any of the data changes.
*/
export class MyCompoundStore
{
/**
* @type {MyStoreData}
*/
#data = Object.seal(new MyStoreData());

/**
* @type {({
* baz: (value: string) => void
* })}
*/
#readableSet;

/**
* @type {Readonly<MyStores>}
*/
#stores;

constructor()
{
const dataWritable = writable(this.#data);

const readableBaz = propertyStore(dataWritable, 'baz');

// This allows you to easily update readable stores internally.
this.#readableSet = Object.freeze({
baz: readableBaz.set
});

// Creates all externally accessible stores.
this.#stores = Object.freeze({
// Writable stores.
foo: propertyStore(dataWritable, 'foo'),
bar: propertyStore(dataWritable, 'bar'),

// Readable stores.
baz: Object.freeze({ subscribe: readableBaz.subscribe })
});
}

/**
* @returns {Readonly<MyStores>} All the stores!
*/
get stores()
{
return this.#stores;
}

/**
* @returns {string} Contrived demo of accessing internal data in stores for manipulation.
*/
get foobar()
{
// You can directly access data here instead of using `get()` from `svelte/store`.
return `${this.#data.foo}${this.#data.bar}`;
}

doSomething()
{
// Calculate stuff... Update readable store...
this.#readableSet.baz('A new baz!');
}
}

/**
* You don't need to create a data class and can use a bare object with a type alias / `@typedef`. In more complex projects it's probably a good idea.
*/
export class MyStoreData
{
constructor()
{
/** @type {string} */
this.foo = 'foo!';

/** @type {string} */
this.bar = 'bar!';

/** @type {string} */
this.baz = 'baz!';
}
}

/**
* @typedef {object} MyStores
*
* @property {import('svelte/store').Writable<string>} foo Foo store
*
* @property {import('svelte/store').Writable<string>} bar Bar store
*
* @property {import('svelte/store').Readable<string>} baz Baz store
*/
// Given an instance of MyCompoundStore
const myCompoundStore = new MyCompoundStore();

// In a Svelte component you do need to destructure the stores into top level
// variables.
const { foo, bar, baz } = myCompoundStore.stores;

$: console.log($foo);
// Given an instance of MyCompoundStore
const myCompoundStore = new MyCompoundStore();

// In a Svelte component you do need to destructure the stores into top level
// variables.
const { foo, bar, baz } = myCompoundStore.stores;

$: console.log($foo);
Heh... I sort of ran out of characters in the post for the above store example. You can add accessors too and they are one way reactive.
// All the other boilerplate from above
export class MyCompoundStore
{
/**
* @returns {string} Foo
*/
get foo() { return this.#data.foo; }

/**
* @param {string} foo - New foo
*/
set foo(foo)
{
if (typeof foo !== 'string') { /* throw errer */ }
this.#stores.foo.set(foo);
}
}
// All the other boilerplate from above
export class MyCompoundStore
{
/**
* @returns {string} Foo
*/
get foo() { return this.#data.foo; }

/**
* @param {string} foo - New foo
*/
set foo(foo)
{
if (typeof foo !== 'string') { /* throw errer */ }
this.#stores.foo.set(foo);
}
}
If you make accessors for all of the internal store properties like that and add a set and subscribe methods for the store contract in MyCompoundStore you can also make it a store that can be subscribed to for any property change. This is what TJSPosition does, but it's just a tad more complex to write out in pseudocode. Theoretically this approach IE having accessors exposed for properties within will somewhat be compatible with a Svelte 5 transition to $state. Instead of accessing / destructuring the .stores accessor you'll be able to transition over to using the individual property accessors for two way reactivity.
Want results from more Discord servers?
Add your server