Pass changed boolean array to derived callbacks

This allows the callbacks to easily run complex update logic based on
which store changed most recently.
pull/6786/head
Robin Munn 5 years ago
parent ca94762fe9
commit 05376a9413

@ -268,7 +268,7 @@ This makes it possible to wrap almost any other reactive state handling library
store = writable(value?: any) store = writable(value?: any)
``` ```
```js ```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)
``` ```
--- ---
@ -295,7 +295,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 ```js
import { writable } from 'svelte/store'; import { writable } from 'svelte/store';
@ -319,7 +319,7 @@ Note that the value of a `writable` is lost when it is destroyed, for example wh
#### `readable` #### `readable`
```js ```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)
``` ```
--- ---
@ -338,6 +338,16 @@ const time = readable(null, set => {
return () => clearInterval(interval); 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` #### `derived`
@ -346,13 +356,13 @@ const time = readable(null, set => {
store = derived(a, callback: (a: any) => any) store = derived(a, callback: (a: any) => any)
``` ```
```js ```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 ```js
store = derived([a, ...b], callback: ([a: any, ...b: any[]]) => any) store = derived([a, ...b], callback: ([a: any, ...b: any[]]) => any)
``` ```
```js ```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, changed: boolean[]) => void | () => void, initial_value: any)
``` ```
--- ---
@ -369,9 +379,9 @@ 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 ```js
import { derived } from 'svelte/store'; import { derived } from 'svelte/store';
@ -379,6 +389,13 @@ import { derived } from 'svelte/store';
const delayed = derived(a, ($a, set) => { const delayed = derived(a, ($a, set) => {
setTimeout(() => set($a), 1000); setTimeout(() => set($a), 1000);
}, 'one moment...'); }, '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
});
``` ```
--- ---
@ -401,7 +418,7 @@ const tick = derived(frequency, ($frequency, set) => {
--- ---
In both cases, an array of arguments can be passed as the first argument instead of a single store. In both cases, an array of stores can be passed as the first argument instead of a single store. In this case, the callback can optionally take a fourth argument, `changed`, which will be an array of Booleans describing which store value(s) changed since the last time the callback was called. (In the single-store case, the `changed` array would be pointless since it would always be equal to `[true]`.)
```js ```js
import { derived } from 'svelte/store'; import { derived } from 'svelte/store';
@ -411,6 +428,19 @@ const summed = derived([a, b], ([$a, $b]) => $a + $b);
const delayed = derived([a, b], ([$a, $b], set) => { const delayed = derived([a, b], ([$a, $b], set) => {
setTimeout(() => set($a + $b), 1000); setTimeout(() => set($a + $b), 1000);
}); });
const loggingSum = derived([a, b], ([$a, $b], set, _, changed) => {
const [aChanged, bChanged] = changed;
if (aChanged) console.log('New value of a', $a);
if (bChanged) console.log('New value of b', $b);
set($a + $b);
});
const complexLogic = derived([a, b], ([$a, $b], set, update, changed) => {
const [aChanged, bChanged] = changed;
if (aChanged) set($a + $b);
if (bChanged) update(n => n * 2 - $b);
});
``` ```
#### `get` #### `get`
@ -808,7 +838,7 @@ The `crossfade` function creates a pair of [transitions](docs#transition_fn) cal
* `delay` (`number`, default 0) — milliseconds before starting * `delay` (`number`, default 0) — milliseconds before starting
* `duration` (`number` | `function`, default 800) — milliseconds the transition lasts * `duration` (`number` | `function`, default 800) — milliseconds the transition lasts
* `easing` (`function`, default `cubicOut`) — an [easing function](docs#svelte_easing) * `easing` (`function`, default `cubicOut`) — an [easing function](docs#svelte_easing)
* `fallback` (`function`) — A fallback [transition](docs#transition_fn) to use for send when there is no matching element being received, and for receive when there is no element being sent. * `fallback` (`function`) — A fallback [transition](docs#transition_fn) to use for send when there is no matching element being received, and for receive when there is no element being sent.
```sv ```sv
<script> <script>

@ -13,7 +13,7 @@ export type Updater<T> = (value: T) => T;
type Invalidator<T> = (value?: T) => void; type Invalidator<T> = (value?: T) => void;
/** Start and stop notification callbacks. */ /** Start and stop notification callbacks. */
export type StartStopNotifier<T> = (set: Subscriber<T>) => Unsubscriber | void; export type StartStopNotifier<T> = (set: Subscriber<T>, update: (fn: Updater<T>) => void) => Unsubscriber | void;
/** Readable interface for subscribing. */ /** Readable interface for subscribing. */
export interface Readable<T> { export interface Readable<T> {
@ -92,7 +92,7 @@ export function writable<T>(value?: T, start: StartStopNotifier<T> = noop): Writ
const subscriber: SubscribeInvalidateTuple<T> = [run, invalidate]; const subscriber: SubscribeInvalidateTuple<T> = [run, invalidate];
subscribers.add(subscriber); subscribers.add(subscriber);
if (subscribers.size === 1) { if (subscribers.size === 1) {
stop = start(set) || noop; stop = start(set, update) || noop;
} }
run(value); run(value);
@ -115,6 +115,34 @@ type Stores = Readable<any> | [Readable<any>, ...Array<Readable<any>>] | Array<R
type StoresValues<T> = T extends Readable<infer U> ? U : type StoresValues<T> = T extends Readable<infer U> ? U :
{ [K in keyof T]: T[K] extends Readable<infer U> ? U : never }; { [K in keyof T]: T[K] extends Readable<infer U> ? U : never };
/**
* Derived value store by synchronizing one or more readable stores and
* applying an aggregation function over its input values.
*
* @param stores - input stores
* @param fn - function callback that aggregates the values
* @param initial_value - when used asynchronously
*/
export function derived<S extends Stores, T>(
stores: S,
fn: (values: StoresValues<S>, set: Subscriber<T>, update: (fn: Updater<T>) => void, changed: boolean[]) => Unsubscriber | void,
initial_value?: T
): Readable<T>;
/**
* Derived value store by synchronizing one or more readable stores and
* applying an aggregation function over its input values.
*
* @param stores - input stores
* @param fn - function callback that aggregates the values
* @param initial_value - when used asynchronously
*/
export function derived<S extends Stores, T>(
stores: S,
fn: (values: StoresValues<S>, set: Subscriber<T>, update: (fn: Updater<T>) => void) => Unsubscriber | void,
initial_value?: T
): Readable<T>;
/** /**
* Derived value store by synchronizing one or more readable stores and * Derived value store by synchronizing one or more readable stores and
* applying an aggregation function over its input values. * applying an aggregation function over its input values.
@ -125,7 +153,7 @@ type StoresValues<T> = T extends Readable<infer U> ? U :
*/ */
export function derived<S extends Stores, T>( export function derived<S extends Stores, T>(
stores: S, stores: S,
fn: (values: StoresValues<S>, set: (value: T) => void) => Unsubscriber | void, fn: (values: StoresValues<S>, set: Subscriber<T>) => Unsubscriber | void,
initial_value?: T initial_value?: T
): Readable<T>; ): Readable<T>;
@ -163,11 +191,12 @@ export function derived<T>(stores: Stores, fn: Function, initial_value?: T): Rea
const auto = fn.length < 2; const auto = fn.length < 2;
return readable(initial_value, (set) => { return readable(initial_value, (set, update) => {
let inited = false; let inited = false;
const values = []; const values = [];
let pending = 0; let pending = 0;
const changed = [];
let cleanup = noop; let cleanup = noop;
const sync = () => { const sync = () => {
@ -175,7 +204,8 @@ export function derived<T>(stores: Stores, fn: Function, initial_value?: T): Rea
return; return;
} }
cleanup(); cleanup();
const result = fn(single ? values[0] : values, set); const result = fn(single ? values[0] : values, set, update, changed);
changed.fill(false);
if (auto) { if (auto) {
set(result as T); set(result as T);
} else { } else {
@ -188,6 +218,7 @@ export function derived<T>(stores: Stores, fn: Function, initial_value?: T): Rea
(value) => { (value) => {
values[i] = value; values[i] = value;
pending &= ~(1 << i); pending &= ~(1 << i);
changed[i] = true;
if (inited) { if (inited) {
sync(); sync();
} }

@ -281,6 +281,45 @@ describe('store', () => {
unsubscribe(); unsubscribe();
}); });
it('provides a boolean array to easily tell what changed', () => {
const count = writable(0);
const values = [];
const a = derived(count, $count => {
return 'a' + $count;
});
const b = derived(count, $count => {
return 'b' + $count;
});
const c = writable(0);
const combined = derived([a, b, c], ([a, b, c], set, _u, changes) => {
const [aChanged, bChanged, cChanged] = changes;
if (aChanged && bChanged) {
set(a + b);
} else if (cChanged) {
set('c' + c);
} else {
set('a or b changed without the other one changing');
}
});
const unsubscribe = combined.subscribe(v => {
values.push(v);
});
assert.deepEqual(values, ['a0b0']);
c.set(2);
count.set(1);
assert.deepEqual(values, ['a0b0', 'c2', 'a1b1']);
unsubscribe();
});
it('derived dependency does not update and shared ancestor updates', () => { it('derived dependency does not update and shared ancestor updates', () => {
const root = writable({ a: 0, b:0 }); const root = writable({ a: 0, b:0 });
const values = []; const values = [];

Loading…
Cancel
Save