Skip to content

Commit

Permalink
feat: pass update function to store setup callbacks (#6750)
Browse files Browse the repository at this point in the history
Fixes #4880.
Fixes #6737.

This will be a breaking change for anyone who uses the StartStopNotifier
type in their / implements custom stores.

---------

Co-authored-by: Simon Holthausen <simon.holthausen@vercel.com>
  • Loading branch information
rmunn and dummdidumm committed May 4, 2023
1 parent ea73930 commit 19e163f
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 13 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
* **breaking** Overhaul and drastically improve creating custom elements with Svelte (see PR for list of changes and migration instructions) ([#8457](https://github.com/sveltejs/svelte/pull/8457))
* **breaking** Deprecate `SvelteComponentTyped`, use `SvelteComponent` instead ([#8512](https://github.com/sveltejs/svelte/pull/8512))
* **breaking** Error on falsy values instead of stores passed to `derived` ([#7947](https://github.com/sveltejs/svelte/pull/7947))
* **breaking** Custom store implementers now need to pass an `update` function additionally to the `set` function ([#6750](https://github.com/sveltejs/svelte/pull/6750))
* Add `a11y no-noninteractive-element-interactions` rule ([#8391](https://github.com/sveltejs/svelte/pull/8391))
* Add `a11y-no-static-element-interactions`rule ([#8251](https://github.com/sveltejs/svelte/pull/8251))
* Bind `null` option and input values consistently ([#8312](https://github.com/sveltejs/svelte/issues/8312))
Expand Down
31 changes: 24 additions & 7 deletions site/content/docs/04-run-time.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,7 +289,7 @@ This makes it possible to wrap almost any other reactive state handling library
store = writable(value?: any)
```
```js
store = writable(value?: any, start?: (set: (value: any) => void) => () => void)
store = writable(value?: any, start?: (set: (value: any) => void, update: (fn: any => any) => void) => () => void)
```

---
Expand All @@ -316,7 +316,7 @@ count.update(n => n + 1); // logs '2'

---

If a function is passed as the second argument, it will be called when the number of subscribers goes from zero to one (but not from one to two, etc). That function will be passed a `set` function which changes the value of the store. It must return a `stop` function that is called when the subscriber count goes from one to zero.
If a function is passed as the second argument, it will be called when the number of subscribers goes from zero to one (but not from one to two, etc). That function will be passed a `set` function which changes the value of the store, and an `update` function which works like the `update` method on the store, taking a callback to calculate the store's new value from its old value. It must return a `stop` function that is called when the subscriber count goes from one to zero.

```js
import { writable } from 'svelte/store';
Expand All @@ -340,7 +340,7 @@ Note that the value of a `writable` is lost when it is destroyed, for example wh
#### `readable`

```js
store = readable(value?: any, start?: (set: (value: any) => void) => () => void)
store = readable(value?: any, start?: (set: (value: any) => void, update: (fn: any => any) => void) => () => void)
```

---
Expand All @@ -359,6 +359,16 @@ const time = readable(null, set => {

return () => clearInterval(interval);
});

const ticktock = readable(null, (set, update) => {
set('tick');

const interval = setInterval(() => {
update(sound => sound === 'tick' ? 'tock' : 'tick');
}, 1000);

return () => clearInterval(interval);
});
```

#### `derived`
Expand All @@ -367,13 +377,13 @@ const time = readable(null, set => {
store = derived(a, callback: (a: any) => any)
```
```js
store = derived(a, callback: (a: any, set: (value: any) => void) => void | () => void, initial_value: any)
store = derived(a, callback: (a: any, set: (value: any) => void, update: (fn: any => any) => void) => void | () => void, initial_value: any)
```
```js
store = derived([a, ...b], callback: ([a: any, ...b: any[]]) => any)
```
```js
store = derived([a, ...b], callback: ([a: any, ...b: any[]], set: (value: any) => void) => void | () => void, initial_value: any)
store = derived([a, ...b], callback: ([a: any, ...b: any[]], set: (value: any) => void, update: (fn: any => any) => void) => void | () => void, initial_value: any)
```

---
Expand All @@ -390,16 +400,23 @@ const doubled = derived(a, $a => $a * 2);

---

The callback can set a value asynchronously by accepting a second argument, `set`, and calling it when appropriate.
The callback can set a value asynchronously by accepting a second argument, `set`, and an optional third argument, `update`, calling either or both of them when appropriate.

In this case, you can also pass a third argument to `derived` — the initial value of the derived store before `set` is first called.
In this case, you can also pass a third argument to `derived` — the initial value of the derived store before `set` or `update` is first called. If no initial value is specified, the store's initial value will be `undefined`.

```js
import { derived } from 'svelte/store';

const delayed = derived(a, ($a, set) => {
setTimeout(() => set($a), 1000);
}, 'one moment...');

const delayedIncrement = derived(a, ($a, set, update) => {
set($a);
setTimeout(() => update(x => x + 1), 1000);
// every time $a produces a value, this produces two
// values, $a immediately and then $a + 1 a second later
});
```

---
Expand Down
13 changes: 7 additions & 6 deletions src/runtime/store/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,11 @@ type Invalidator<T> = (value?: T) => void;
* This function is called when the first subscriber subscribes.
*
* @param {(value: T) => void} set Function that sets the value of the store.
* @param {(value: Updater<T>) => void} set Function that sets the value of the store after passing the current value to the update function.
* @returns {void | (() => void)} Optionally, a cleanup function that is called when the last remaining
* subscriber unsubscribes.
*/
export type StartStopNotifier<T> = (set: (value: T) => void) => void | (() => void);
export type StartStopNotifier<T> = (set: (value: T) => void, update: (fn: Updater<T>) => void) => void | (() => void);

/** Readable interface for subscribing. */
export interface Readable<T> {
Expand Down Expand Up @@ -99,7 +100,7 @@ export function writable<T>(value?: T, start: StartStopNotifier<T> = noop): Writ
const subscriber: SubscribeInvalidateTuple<T> = [run, invalidate];
subscribers.add(subscriber);
if (subscribers.size === 1) {
stop = start(set) || noop;
stop = start(set, update) || noop;
}
run(value);

Expand Down Expand Up @@ -130,9 +131,9 @@ type StoresValues<T> = T extends Readable<infer U> ? U :
* @param fn - function callback that aggregates the values
* @param initial_value - when used asynchronously
*/
export function derived<S extends Stores, T>(
export function derived<S extends Stores, T>(
stores: S,
fn: (values: StoresValues<S>, set: (value: T) => void) => Unsubscriber | void,
fn: (values: StoresValues<S>, set: Subscriber<T>, update: (fn: Updater<T>) => void) => Unsubscriber | void,
initial_value?: T
): Readable<T>;

Expand Down Expand Up @@ -171,7 +172,7 @@ export function derived<T>(stores: Stores, fn: Function, initial_value?: T): Rea

const auto = fn.length < 2;

return readable(initial_value, (set) => {
return readable(initial_value, (set, update) => {
let started = false;
const values = [];

Expand All @@ -183,7 +184,7 @@ export function derived<T>(stores: Stores, fn: Function, initial_value?: T): Rea
return;
}
cleanup();
const result = fn(single ? values[0] : values, set);
const result = fn(single ? values[0] : values, set, update);
if (auto) {
set(result as T);
} else {
Expand Down
77 changes: 77 additions & 0 deletions test/store/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,50 @@ describe('store', () => {
assert.deepEqual(values, [0, 1, 2]);
});

it('passes an optional update function', () => {
let running;
let tick;
let add;

const store = readable(undefined, (set, update) => {
tick = set;
running = true;
add = n => update(value => value + n);

set(0);

return () => {
tick = () => { };
add = _ => { };
running = false;
};
});

assert.ok(!running);

const values = [];

const unsubscribe = store.subscribe(value => {
values.push(value);
});

assert.ok(running);
tick(1);
tick(2);
add(3);
add(4);
tick(5);
add(6);

unsubscribe();

assert.ok(!running);
tick(7);
add(8);

assert.deepEqual(values, [0, 1, 2, 5, 9, 5, 11]);
});

it('creates an undefined readable store', () => {
const store = readable();
const values = [];
Expand Down Expand Up @@ -241,6 +285,39 @@ describe('store', () => {
assert.deepEqual(values, [0, 2, 4]);
});

it('passes optional set and update functions', () => {
const number = writable(1);
const evensAndSquaresOf4 = derived(number, (n, set, update) => {
if (n % 2 === 0) set(n);
if (n % 4 === 0) update(n => n * n);
}, 0);

const values = [];

const unsubscribe = evensAndSquaresOf4.subscribe(value => {
values.push(value);
});

number.set(2);
number.set(3);
number.set(4);
number.set(5);
number.set(6);
assert.deepEqual(values, [0, 2, 4, 16, 6]);

number.set(7);
number.set(8);
number.set(9);
number.set(10);
assert.deepEqual(values, [0, 2, 4, 16, 6, 8, 64, 10]);

unsubscribe();

number.set(11);
number.set(12);
assert.deepEqual(values, [0, 2, 4, 16, 6, 8, 64, 10]);
});

it('prevents glitches', () => {
const lastname = writable('Jekyll');
const firstname = derived(lastname, n => n === 'Jekyll' ? 'Henry' : 'Edward');
Expand Down

0 comments on commit 19e163f

Please sign in to comment.