Skip to content

Commit

Permalink
replace getters with explicit functions and change makeDerivedStore f…
Browse files Browse the repository at this point in the history
…irst parameter from array to key-value object
  • Loading branch information
cdellacqua committed Aug 12, 2022
1 parent 5b9b216 commit 5de5096
Show file tree
Hide file tree
Showing 12 changed files with 377 additions and 196 deletions.
104 changes: 104 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Changelog

## V1 to V2

1. nOfSubscriptions has become a function instead of a getter property;
2. value has been replaced by content, which is also a function now;
3. makeDerivedStore now accepts an object literal instead of an array
as its first argument;

### From getters to explicit functions (changes 1 and 2)

These changes have been made to remove common pitfalls that can occur
during development related to the spread object syntax. In particular,
destructuring a store would previously cause nOfSubscription and value
to return the current snapshot of the store state, which is rarely what's
needed. Returning a function will let you choose whether or not you
want a snapshot or something you can call at any moment to get the
most up-to-date information.

Example:

In V1 this simple use case would have caused an unexpected behaviour:

```ts
const base$ = makeStore(0);
const extended$ = {
...base$,
increment() {
base$.update((n) => n + 1);
},
};
console.log(extended$.value); // 0
extended$.increment();
console.log(extended$.value); // 0 !!! [incorrect]
```

The correct implementation in V1 would have been:

```ts
const base$ = makeStore(0);
const extended$ = {
...base$,
increment() {
base$.update((n) => n + 1);
}
get value() {
return base$.value;
}
get nOfSubscriptions() {
return base$.nOfSubscriptions;
}
}
console.log(extended$.value); // 0
extended$.increment();
console.log(extended$.value); // 1 [correct]
```

Delaying the access to the property is the solution, but it also adds the overhead of
an extra function call every time nesting happens. To fix this issue and make the composition more intuitive, in V2 you can simply do as follows, and it will work as expected:

V2

```ts
const base$ = makeStore(0);
const extended$ = {
...base$,
increment() {
base$.update((n) => n + 1);
},
};
console.log(extended$.content()); // 0
extended$.increment();
console.log(extended$.content()); // 1 [correct]
```

### makeDerivedStore, from array to object (change 3)

makeDerivedStore is really useful, especially when you need to
combine the content of multiple stores.

When deriving multiple stores, though, it's easy to lose track of the indices,
especially when modifying existing code. Changing the argument of makeDerivedStore from an array to an object solves this problem and it also lets TypeScript validate the argument thoroughly.

Example:

This code in V1 wouldn't have emitted any compile-time error:

```ts
const source1$ = makeStore(6);
const source2$ = makeStore(3);
const derived$ = makeDerivedStore([source1$, source2$], ([v1, v2, v3]) => v1 + v2 + v3);
console.log(derived$.value); // NaN, because the computation above would resolve to 6 + 3 + undefined
```

In V2 the same functionality can be rewritten as follows:

```ts
const source1$ = makeStore(6);
const source2$ = makeStore(3);
// The commented code below would cause a TypeScript error, specifically "Property 'v3' does not exist on type '{ v1: number; v2: number; }'"
// const derived$ = makeDerivedStore({v1: source1$, v2: source2$}, ({v1, v2, v3}) => v1 + v2 + v3);
const derived$ = makeDerivedStore({v1: source1$, v2: source2$}, ({v1, v2}) => v1 + v2);
console.log(derived$.content()); // 9, because the computation above would resolve to 6 + 3
```
55 changes: 44 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ This package provides a framework-agnostic implementation of this concept.

[Documentation](./docs/README.md)

## Migrating to V2

Please refer to the [changelog](./CHANGELOG.md).

## Store

In a nutshell, stores are observable containers of values.
Expand All @@ -29,9 +33,9 @@ There is also a getter `value` that retrieves the current value of the store:
import {makeStore} from 'universal-stores';

const store$ = makeStore(0);
console.log(store$.value); // 0
console.log(store$.content()); // 0
store$.set(1);
console.log(store$.value); // 1
console.log(store$.content()); // 1
```

When a subscriber is attached to a store it immediately receives the current value.
Expand All @@ -46,7 +50,7 @@ Let's see an example:
import {makeStore} from 'universal-stores';

const store$ = makeStore(0);
console.log(store$.value); // 0
console.log(store$.content()); // 0
const unsubscribe = store$.subscribe((v) => console.log(v)); // immediately prints 0
store$.set(1); // triggers the above subscriber, printing 1
unsubscribe();
Expand Down Expand Up @@ -80,11 +84,11 @@ const subscriber = (v: number) => console.log(v);
const unsubscribe1 = store$.subscribe(subscriber); // prints 0
const unsubscribe2 = store$.subscribe(subscriber); // prints 0
const unsubscribe3 = store$.subscribe(subscriber); // prints 0
console.log(store$.nOfSubscriptions); // 1
console.log(store$.nOfSubscriptions()); // 1
unsubscribe3(); // will remove "subscriber"
unsubscribe2(); // won't do anything, "subscriber" has already been removed
unsubscribe1(); // won't do anything, "subscriber" has already been removed
console.log(store$.nOfSubscriptions); // 0
console.log(store$.nOfSubscriptions()); // 0
```

If you ever needed to add the same function
Expand All @@ -95,15 +99,15 @@ import {makeStore} from 'universal-stores';

const store$ = makeStore(0);
const subscriber = (v: number) => console.log(v);
console.log(store$.nOfSubscriptions); // 0
console.log(store$.nOfSubscriptions()); // 0
const unsubscribe1 = store$.subscribe(subscriber); // prints 0
console.log(store$.nOfSubscriptions); // 1
console.log(store$.nOfSubscriptions()); // 1
const unsubscribe2 = store$.subscribe((v) => subscriber(v)); // prints 0
console.log(store$.nOfSubscriptions); // 2
console.log(store$.nOfSubscriptions()); // 2
unsubscribe2();
console.log(store$.nOfSubscriptions); // 1
console.log(store$.nOfSubscriptions()); // 1
unsubscribe1();
console.log(store$.nOfSubscriptions); // 0
console.log(store$.nOfSubscriptions()); // 0
```

## Deriving
Expand All @@ -130,7 +134,7 @@ import {makeStore, makeDerivedStore} from 'universal-stores';

const firstWord$ = makeStore('hello');
const secondWord$ = makeStore('world!');
const derived$ = makeDerivedStore([firstWord$, secondWord$], ([first, second]) => `${first} ${second}`);
const derived$ = makeDerivedStore({first: firstWord$, second: secondWord$}, ({first, second}) => `${first} ${second}`);
derived$.subscribe((v) => console.log(v)); // prints "hello world!"
firstWord$.set('hi'); // will trigger console.log, printing "hi world!"
```
Expand Down Expand Up @@ -165,6 +169,7 @@ import {makeReadonlyStore} from 'universal-stores';

const oneHertzPulse$ = makeReadonlyStore<number>(undefined, (set) => {
console.log('start');
set(performance.now());
const interval = setInterval(() => {
set(performance.now());
}, 1000);
Expand Down Expand Up @@ -230,6 +235,34 @@ objectStore$.set({veryLongText: '...', hash: 0xbbdd}); // will trigger objectSto
objectStore$.set({veryLongText: '...', hash: 0xbbdd}); // will only trigger objectStore$ subscribers
```

## Adding behaviour

If you need to encapsulate behaviour in a custom store, you
can simply destructure a regular store and add your
custom methods to the already existing ones.

Example:

```ts
import {makeStore} from 'universal-stores';

function makeCounterStore(): ReadonlyStore<number> & {increment(): void} {
const {subscribe, content, update, nOfSubscriptions} = makeStore(0);
return {
subscribe,
content,
nOfSubscriptions,
increment() {
update((n) => n + 1);
},
};
}

const counter$ = makeCounterStore();
counter$.subscribe(console.log); // immediately prints 0
counter$.increment(); // will trigger the above console.log, printing 1
```

## Motivation

UI frameworks often ship with their own state management layer,
Expand Down
32 changes: 16 additions & 16 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,8 @@ therefore its value can only be changed by a [StartHandler](README.md#starthandl

| Name | Type |
| :------ | :------ |
| ``get` **nOfSubscriptions**(): `number`` | {} |
| ``get` **value**(): `T`` | {} |
| `content` | () => `T` |
| `nOfSubscriptions` | () => `number` |
| `subscribe` | (`subscriber`: [`Subscriber`](README.md#subscriber)<`T`\>) => [`Unsubscribe`](README.md#unsubscribe) |

#### Defined in
Expand Down Expand Up @@ -430,7 +430,7 @@ Example usage:
```ts
const source1$ = makeStore(10);
const source2$ = makeStore(-10);
const derived$ = makeDerivedStore([source1$, source2$], ([v1, v2]) => v1 + v2);
const derived$ = makeDerivedStore({v1: source1$, v2: source2$}, ({v1, v2}) => v1 + v2);
source1$.subscribe((v) => console.log(v)); // prints 10
source2$.subscribe((v) => console.log(v)); // prints -10
derived$.subscribe((v) => console.log(v)); // prints 0
Expand All @@ -442,15 +442,15 @@ source2$.set(9); // prints 9 (second console.log) and 20 (third console.log)

| Name | Type |
| :------ | :------ |
| `TIn` | extends [`unknown`, ...unknown[]] |
| `TIn` | extends `Record`<`string`, `unknown`\> |
| `TOut` | `TOut` |

#### Parameters

| Name | Type | Description |
| :------ | :------ | :------ |
| `readonlyStores` | { [P in string \| number \| symbol]: ReadonlyStore<TIn[P]\> } | an array of stores or readonly stores. |
| `map` | (`values`: `TIn`) => `TOut` | a function that takes the current value of all the source stores and maps it to another value. |
| `readonlyStores` | { [K in string \| number \| symbol]: ReadonlyStore<TIn[K]\> } | an array of stores or readonly stores. |
| `map` | (`value`: { [K in string \| number \| symbol]: TIn[K] }) => `TOut` | a function that takes the current value of all the source stores and maps it to another value. |
| `config?` | [`DerivedStoreConfig`](README.md#derivedstoreconfig)<`TOut`\> | - |

#### Returns
Expand All @@ -476,9 +476,9 @@ const store$ = makeReadonlyStore(value, (set) => {
value++;
set(value);
});
console.log(store$.value); // 1
console.log(store$.content()); // 1
store$.subscribe((v) => console.log(v)); // immediately prints 2
console.log(store$.value); // 2
console.log(store$.content()); // 2
```

#### Type parameters
Expand All @@ -502,7 +502,7 @@ a ReadonlyStore

#### Defined in

[index.ts:227](https://github.com/cdellacqua/stores.js/blob/main/src/lib/index.ts#L227)
[index.ts:223](https://github.com/cdellacqua/stores.js/blob/main/src/lib/index.ts#L223)

**makeReadonlyStore**<`T`\>(`initialValue`, `config?`): [`ReadonlyStore`](README.md#readonlystore)<`T`\>

Expand Down Expand Up @@ -539,7 +539,7 @@ a ReadonlyStore

#### Defined in

[index.ts:245](https://github.com/cdellacqua/stores.js/blob/main/src/lib/index.ts#L245)
[index.ts:241](https://github.com/cdellacqua/stores.js/blob/main/src/lib/index.ts#L241)

**makeReadonlyStore**<`T`\>(`initialValue`, `startOrConfig?`): [`ReadonlyStore`](README.md#readonlystore)<`T`\>

Expand All @@ -552,9 +552,9 @@ const store$ = makeReadonlyStore(value, (set) => {
value++;
set(value);
});
console.log(store$.value); // 1
console.log(store$.content()); // 1
store$.subscribe((v) => console.log(v)); // immediately prints 2
console.log(store$.value); // 2
console.log(store$.content()); // 2
```

#### Type parameters
Expand All @@ -578,7 +578,7 @@ a ReadonlyStore

#### Defined in

[index.ts:265](https://github.com/cdellacqua/stores.js/blob/main/src/lib/index.ts#L265)
[index.ts:261](https://github.com/cdellacqua/stores.js/blob/main/src/lib/index.ts#L261)

___

Expand All @@ -591,7 +591,7 @@ Make a store of type T.
Example usage:
```ts
const store$ = makeStore(0);
console.log(store$.value); // 0
console.log(store$.content()); // 0
store$.subscribe((v) => console.log(v));
store$.set(10); // will trigger the above console log, printing 10
```
Expand Down Expand Up @@ -626,7 +626,7 @@ Make a store of type T.
Example usage:
```ts
const store$ = makeStore(0);
console.log(store$.value); // 0
console.log(store$.content()); // 0
store$.subscribe((v) => console.log(v));
store$.set(10); // will trigger the above console log, printing 10
```
Expand Down Expand Up @@ -661,7 +661,7 @@ Make a store of type T.
Example usage:
```ts
const store$ = makeStore(0);
console.log(store$.value); // 0
console.log(store$.content()); // 0
store$.subscribe((v) => console.log(v));
store$.set(10); // will trigger the above console log, printing 10
```
Expand Down
14 changes: 7 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,6 @@
"vite": "^2.6.4"
},
"dependencies": {
"@cdellacqua/signals": "^4.1.1"
"@cdellacqua/signals": "^5.0.0"
}
}
2 changes: 1 addition & 1 deletion src/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const appDiv = document.getElementById('app') as HTMLDivElement;

const random1$ = makeStore(0);
const random2$ = makeStore(0);
const sum$ = makeDerivedStore([random1$, random2$], ([r1, r2]) => r1 + r2);
const sum$ = makeDerivedStore({r1: random1$, r2: random2$}, ({r1, r2}) => r1 + r2);

const span1 = document.createElement('span');
appDiv.appendChild(span1);
Expand Down
Loading

0 comments on commit 5de5096

Please sign in to comment.