Skip to content

Commit 19e163f

Browse files
rmunndummdidumm
andauthored
feat: pass update function to store setup callbacks (#6750)
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 <[email protected]>
1 parent ea73930 commit 19e163f

File tree

4 files changed

+109
-13
lines changed

4 files changed

+109
-13
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
* **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))
1212
* **breaking** Deprecate `SvelteComponentTyped`, use `SvelteComponent` instead ([#8512](https://github.com/sveltejs/svelte/pull/8512))
1313
* **breaking** Error on falsy values instead of stores passed to `derived` ([#7947](https://github.com/sveltejs/svelte/pull/7947))
14+
* **breaking** Custom store implementers now need to pass an `update` function additionally to the `set` function ([#6750](https://github.com/sveltejs/svelte/pull/6750))
1415
* Add `a11y no-noninteractive-element-interactions` rule ([#8391](https://github.com/sveltejs/svelte/pull/8391))
1516
* Add `a11y-no-static-element-interactions`rule ([#8251](https://github.com/sveltejs/svelte/pull/8251))
1617
* Bind `null` option and input values consistently ([#8312](https://github.com/sveltejs/svelte/issues/8312))

site/content/docs/04-run-time.md

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,7 @@ This makes it possible to wrap almost any other reactive state handling library
289289
store = writable(value?: any)
290290
```
291291
```js
292-
store = writable(value?: any, start?: (set: (value: any) => void) => () => void)
292+
store = writable(value?: any, start?: (set: (value: any) => void, update: (fn: any => any) => void) => () => void)
293293
```
294294

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

317317
---
318318

319-
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.
319+
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.
320320

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

342342
```js
343-
store = readable(value?: any, start?: (set: (value: any) => void) => () => void)
343+
store = readable(value?: any, start?: (set: (value: any) => void, update: (fn: any => any) => void) => () => void)
344344
```
345345

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

360360
return () => clearInterval(interval);
361361
});
362+
363+
const ticktock = readable(null, (set, update) => {
364+
set('tick');
365+
366+
const interval = setInterval(() => {
367+
update(sound => sound === 'tick' ? 'tock' : 'tick');
368+
}, 1000);
369+
370+
return () => clearInterval(interval);
371+
});
362372
```
363373

364374
#### `derived`
@@ -367,13 +377,13 @@ const time = readable(null, set => {
367377
store = derived(a, callback: (a: any) => any)
368378
```
369379
```js
370-
store = derived(a, callback: (a: any, set: (value: any) => void) => void | () => void, initial_value: any)
380+
store = derived(a, callback: (a: any, set: (value: any) => void, update: (fn: any => any) => void) => void | () => void, initial_value: any)
371381
```
372382
```js
373383
store = derived([a, ...b], callback: ([a: any, ...b: any[]]) => any)
374384
```
375385
```js
376-
store = derived([a, ...b], callback: ([a: any, ...b: any[]], set: (value: any) => void) => void | () => void, initial_value: any)
386+
store = derived([a, ...b], callback: ([a: any, ...b: any[]], set: (value: any) => void, update: (fn: any => any) => void) => void | () => void, initial_value: any)
377387
```
378388

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

391401
---
392402

393-
The callback can set a value asynchronously by accepting a second argument, `set`, and calling it when appropriate.
403+
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.
394404

395-
In this case, you can also pass a third argument to `derived` — the initial value of the derived store before `set` is first called.
405+
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`.
396406

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

400410
const delayed = derived(a, ($a, set) => {
401411
setTimeout(() => set($a), 1000);
402412
}, 'one moment...');
413+
414+
const delayedIncrement = derived(a, ($a, set, update) => {
415+
set($a);
416+
setTimeout(() => update(x => x + 1), 1000);
417+
// every time $a produces a value, this produces two
418+
// values, $a immediately and then $a + 1 a second later
419+
});
403420
```
404421

405422
---

src/runtime/store/index.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,11 @@ type Invalidator<T> = (value?: T) => void;
1717
* This function is called when the first subscriber subscribes.
1818
*
1919
* @param {(value: T) => void} set Function that sets the value of the store.
20+
* @param {(value: Updater<T>) => void} set Function that sets the value of the store after passing the current value to the update function.
2021
* @returns {void | (() => void)} Optionally, a cleanup function that is called when the last remaining
2122
* subscriber unsubscribes.
2223
*/
23-
export type StartStopNotifier<T> = (set: (value: T) => void) => void | (() => void);
24+
export type StartStopNotifier<T> = (set: (value: T) => void, update: (fn: Updater<T>) => void) => void | (() => void);
2425

2526
/** Readable interface for subscribing. */
2627
export interface Readable<T> {
@@ -99,7 +100,7 @@ export function writable<T>(value?: T, start: StartStopNotifier<T> = noop): Writ
99100
const subscriber: SubscribeInvalidateTuple<T> = [run, invalidate];
100101
subscribers.add(subscriber);
101102
if (subscribers.size === 1) {
102-
stop = start(set) || noop;
103+
stop = start(set, update) || noop;
103104
}
104105
run(value);
105106

@@ -130,9 +131,9 @@ type StoresValues<T> = T extends Readable<infer U> ? U :
130131
* @param fn - function callback that aggregates the values
131132
* @param initial_value - when used asynchronously
132133
*/
133-
export function derived<S extends Stores, T>(
134+
export function derived<S extends Stores, T>(
134135
stores: S,
135-
fn: (values: StoresValues<S>, set: (value: T) => void) => Unsubscriber | void,
136+
fn: (values: StoresValues<S>, set: Subscriber<T>, update: (fn: Updater<T>) => void) => Unsubscriber | void,
136137
initial_value?: T
137138
): Readable<T>;
138139

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

172173
const auto = fn.length < 2;
173174

174-
return readable(initial_value, (set) => {
175+
return readable(initial_value, (set, update) => {
175176
let started = false;
176177
const values = [];
177178

@@ -183,7 +184,7 @@ export function derived<T>(stores: Stores, fn: Function, initial_value?: T): Rea
183184
return;
184185
}
185186
cleanup();
186-
const result = fn(single ? values[0] : values, set);
187+
const result = fn(single ? values[0] : values, set, update);
187188
if (auto) {
188189
set(result as T);
189190
} else {

test/store/index.js

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,50 @@ describe('store', () => {
137137
assert.deepEqual(values, [0, 1, 2]);
138138
});
139139

140+
it('passes an optional update function', () => {
141+
let running;
142+
let tick;
143+
let add;
144+
145+
const store = readable(undefined, (set, update) => {
146+
tick = set;
147+
running = true;
148+
add = n => update(value => value + n);
149+
150+
set(0);
151+
152+
return () => {
153+
tick = () => { };
154+
add = _ => { };
155+
running = false;
156+
};
157+
});
158+
159+
assert.ok(!running);
160+
161+
const values = [];
162+
163+
const unsubscribe = store.subscribe(value => {
164+
values.push(value);
165+
});
166+
167+
assert.ok(running);
168+
tick(1);
169+
tick(2);
170+
add(3);
171+
add(4);
172+
tick(5);
173+
add(6);
174+
175+
unsubscribe();
176+
177+
assert.ok(!running);
178+
tick(7);
179+
add(8);
180+
181+
assert.deepEqual(values, [0, 1, 2, 5, 9, 5, 11]);
182+
});
183+
140184
it('creates an undefined readable store', () => {
141185
const store = readable();
142186
const values = [];
@@ -241,6 +285,39 @@ describe('store', () => {
241285
assert.deepEqual(values, [0, 2, 4]);
242286
});
243287

288+
it('passes optional set and update functions', () => {
289+
const number = writable(1);
290+
const evensAndSquaresOf4 = derived(number, (n, set, update) => {
291+
if (n % 2 === 0) set(n);
292+
if (n % 4 === 0) update(n => n * n);
293+
}, 0);
294+
295+
const values = [];
296+
297+
const unsubscribe = evensAndSquaresOf4.subscribe(value => {
298+
values.push(value);
299+
});
300+
301+
number.set(2);
302+
number.set(3);
303+
number.set(4);
304+
number.set(5);
305+
number.set(6);
306+
assert.deepEqual(values, [0, 2, 4, 16, 6]);
307+
308+
number.set(7);
309+
number.set(8);
310+
number.set(9);
311+
number.set(10);
312+
assert.deepEqual(values, [0, 2, 4, 16, 6, 8, 64, 10]);
313+
314+
unsubscribe();
315+
316+
number.set(11);
317+
number.set(12);
318+
assert.deepEqual(values, [0, 2, 4, 16, 6, 8, 64, 10]);
319+
});
320+
244321
it('prevents glitches', () => {
245322
const lastname = writable('Jekyll');
246323
const firstname = derived(lastname, n => n === 'Jekyll' ? 'Henry' : 'Edward');

0 commit comments

Comments
 (0)