From 9ed403081d0085fcca6a39a60af4b7186405b852 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 10 Nov 2018 22:43:58 -0500 Subject: [PATCH 1/8] add observables RFC --- text/0002-observables.md | 472 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 472 insertions(+) create mode 100644 text/0002-observables.md diff --git a/text/0002-observables.md b/text/0002-observables.md new file mode 100644 index 0000000..fd6df9d --- /dev/null +++ b/text/0002-observables.md @@ -0,0 +1,472 @@ +- Start Date: 2018-11-10 +- RFC PR: (leave this empty) +- Svelte Issue: (leave this empty) + +# Svelte observables + +## Summary + +This RFC proposes a replacement for the existing [Store](https://svelte.technology/guide#state-management) class that satisfies the following goals: + +* Works with the Svelte 3 component design outlined in [RFC 1](https://github.com/sveltejs/rfcs/pull/4) +* Allows any given component to subscribe to multiple sources of data from outside the component tree, rather than favouring a single app-level megastore +* Typechecker friendliness +* Adapts to existing state management systems like [MobX](https://mobx.js.org/) or [TC39 Observables](https://github.com/tc39/proposal-observable), but does not require them +* Concise syntax that eliminates lifecycle boilerplate + + +## Motivation + +In Svelte 1, we introduced the Store class, which provides a simple app-level state management solution with an API that mirrors that of individual components — `get`, `set`, `on`, `fire` and so on. The store can be attached to a component tree (or subtree), at which point it becomes available within lifecycle hooks and methods as `this.store`, and in templates by prefixing identifiers with `$`. Svelte is then able to subscribe to changes to app-level data with reasonable granularity and zero boilerplate. + +In Svelte 2, Store support was turned on by default. + +This approach works reasonably well but has some drawbacks: + +* It doesn't permit namespacing or nesting; the store becomes a grab-bag of values +* Because of the convenience of the store being auto-attached to components, it becomes a magnet for things that aren't purely related to application state, such as effectful `login` and `logout` methods +* Setting deep properties (`foo.bar.baz` etc) is cumbersome +* It doesn't play well with TypeScript +* Creating derived ('computed') values involves a slightly weird API +* Working with existing state management systems isn't as natural as it should be + +A large part of the reason for the design of the original store — the fact that it auto-attaches to components, for example — is that the cost of using imported objects in Svelte 1 and 2 is unreasonably high. In Svelte 3, per [RFC 1](https://github.com/sveltejs/rfcs/pull/4), that cost will be significantly reduced. We can therefore pursue alternatives without the aforementioned drawbacks. + + +## Detailed design + +Essentially, the shift is from a single observable store of values to multiple observable values. Instead of this... + +```js +export default new Store({ + user: { + firstname: 'Fozzie', + lastname: 'Bear + }, + volume: 0.5 +}); +``` + +...we have an `observable` (🐃) function: + +```js +export const user = observable({ + firstname: 'Fozzie', + lastname: 'Bear +}); + +export const volume = observable(0.5); +``` + +> The 🐃 emoji, used throughout this document, indicates a yak that needs shaving + +Interested parties can read (and write) `user` without caring about `volume`, and vice versa: + +```js +import { volume } from './observables.js'; + +const audio = document.querySelector('audio'); + +const unsubscribe = volume.subscribe(value => { + audio.volume = value; +}); +``` + +Inside a component's markup, a convenient shorthand sets up the necessary subscriptions (and unsubscribes when the component is destroyed) — the `$` prefix: + +```js + + +

Hello {$user.firstname}!

+``` + +This would compile to something like the following: + +```js +import { ondestroy } from 'svelte'; +import { user } from './observables.js'; + +const Component = defineComponent((__update) => { + let $user; + ondestroy(user.subscribe(value => { + $user = value; + __update({ $user: true }); + })); + + return () => ({ $user }); +}, create_main_fragment); +``` + + +### Observable API + +An observable *must* have a `subscribe` method, and it may also have additional methods like `set` and `update` if it isn't read-only: + +```js +const number = observable(1); + +const unsubscribe = number.subscribe(value => { + console.log(`value is ${value}`); // logs 1 immediately +}); + +number.set(2); // logs 2 + +const incr = n => n + 1; +number.update(incr); // logs 3 + +unsubscribe(); +number.set(4); // does nothing — unsubscribed +``` + +An example implementation of this API: + +```js +function observable(value) { + const subscribers = []; + + function set(newValue) { + if (newValue === value) return; + value = newValue; + subscribers.forEach(fn => fn(value)); + } + + function update(fn) { + set(fn(value)); + } + + function subscribe(fn) { + subscribers.push(fn); + fn(value); + + return function() { + const index = subscribers.indexOf(fn); + if (index !== -1) subscribers.splice(index, 1); + } + } + + return { set, update, subscribe }; +} +``` + + +### Read-only observables + +Some observables are read-only, created with `readOnlyObservable` (🐃): + +```js +const unsubscribe = mousePosition.subscribe(pos => { + if (pos) console.log(pos.x, pos.y); +}); + +mousePosition.set({ x: 100, y: 100 }); // Error: mousePosition.set is not a function +``` + +An example implementation: + +```js +function readOnlyObservable(start, value) { + const subscribers = []; + let stop; + + function set(newValue) { + if (newValue === value) return; + value = newValue; + subscribers.forEach(fn => fn(value)); + } + + return { + subscribe(fn) { + if (subscribers.length === 0) { + stop = start(set); + } + + subscribers.push(fn); + fn(value); + + return function() { + const index = subscribers.indexOf(fn); + if (index !== -1) subscribers.splice(index, 1); + + if (subscribers.length === 0) { + stop(); + stop = null; + } + }; + } + } +} + +const mousePosition = readOnlyObservable(function start(set) { + function handler(event) { + set({ + x: event.clientX, + y: event.clientY + }); + } + + document.addEventListener('mousemove', handler); + return function stop() { + document.removeEventListener('mousemove', handler); + } +}); +``` + +### Derived observables + +An observable can be derived from other observables with `derivedObservable` (🐃): + +```js +const a = observable(1); +const b = observable(2); +const c = observable(3); + +const total = derivedObservable(a, b, c, (a, b, c) => a + b + c); + +total.subscribe(value => { + console.log(`total is ${value}`); // logs 'total is 6' +}); + +c.set(4); // logs 'total is 7' +``` + +Example implementation: + +```js +function derivedObservable(...args) { + const fn = args.pop(); + + return readOnlyObservable(set => { + let inited = false; + const values = []; + + const unsubscribers = args.map((arg, i) => arg.subscribe(value => { + values[i] = value; + if (inited) set(fn(...values)); + })); + + inited = true; + set(fn(...values)); + + return function stop() { + unsubscribers.forEach(fn => fn()); + }; + }); +} +``` + +> In the example above, `total` is recalculated immediately whenever the values of `a`, `b` or `c` are set. In some situations that's undesirable; you want to be able to set `a`, `b` *and* `c` without `total` being recalculated until you've finished. That could be done by putting the `set(fn(...values))` in a microtask, but that has drawbacks too. (Of course, that could be a decision left to the user.) Is this a fatal flaw in the design — should we strive for pull-based rather than push-based derived values? Or is it fine in reality? + +Derived observables are, by nature, also read-only. They could be used, for example, to filter the items in a todo list: + +```html + + + + +{#each $filtered as todo} +

{todo.description}

+{/each} +``` + + +### Relationship with TC39 Observables + +There is a [stage 1 proposal for an Observable object](https://github.com/tc39/proposal-observable) in JavaScript itself. It's quite different from the design documented here, which suggests we might need to come up with an alternative name. For now, I'll distinguish between an 'Observable' (TC39) and an 'observable' (us). + +Cards on the table: I'm not personally a fan of Observables. I've found them to be confusing and awkward to work with. But there are particular reasons why I don't think they're a good general solution for representing reactive values in a component: + +* They don't represent a single value changing over time, but rather a stream of distinct values. This is a subtle but important distinction +* Two different subscribers to the same observable could receive different values (!), where as in a UI you want two references to the same value to be guaranteed to be consistent +* Observables can 'complete', but declarative components (in Svelte and other frameworks) deliberately do not have a concept of time. The two things are incompatible +* They have error-handling semantics that are very often redundant (what error could occur when observing the mouse position, for example?). When they're not redundant (e.g. in the case of data coming over the network), errors are perhaps best handled out-of-band, since the goal is to concisely represent the value in a component template + +Of course, some Observables *are* suitable for representing reactive values in a template, and they could easily be adapted to work with this design: + +```js +function adaptor(observable) { + return { + subscribe(fn) { + observable.subscribe({ + next: fn + }); + + return subscriber.unsubscribe; + } + } +} + +const tc39Observable = Observable.of('red', 'green', 'blue'); +const svelteObservable = adaptor(tc39Observable); + +const unsubscribe = svelteObservable.subscribe(color => { + console.log(color); // logs red, then green, then blue +}); +``` + + +### Examples of use with existing state management libraries + +More broadly, the same technique will work with existing state management libraries, as long as they expose the necessary hooks for observing changes. (I've found this to be difficult with MobX, but perhaps I'm just not sufficiently familiar with that library — would welcome submissions.) + + +#### Redux + +```js +// src/redux.js +import { createStore } from 'redux'; + +export const store = createStore((state = 0, action) => { + switch (action.type) { + case 'INCREMENT': + return state + 1 + case 'DECREMENT': + return state - 1 + default: + return state + } +}); + +function adaptor(store) { + return { + subscribe(fn) { + return store.subscribe(() => { + fn(store.getState()); + }); + } + }; +} + +export const observable = adaptor(store); +``` + +```html + + + + +``` + + +#### Immer + +```js +import { produce } from 'immer'; + +function immerObservable(data) { + const o = observable(data); + + function update(fn) { + o.update(state => produce(state, fn)); + } + + return { + update, + subscribe: o.subscribe + }; +} + +const todos = immerObservable([ + { done: false, description: 'walk the dog' }, + { done: false, description: 'mow the lawn' }, + { done: false, description: 'do the laundry' } +]); + +todos.update(draft => { + draft[0].done = true; +}); +``` + + +#### Shiz + +```js +import { value, computed } from 'shiz'; + +const a = value(1); +const b = computed([a], ([a]) => a * 2); + +function shizObservable(shiz) { + return readOnlyObservable(function start(set) { + return shiz.on('change', () => { + set(shiz.get()); + }); + }, shiz.get()); +} + +const observable = shizObservable(b); + +const unsubscribe = observable.subscribe(value => { + console.log(value); // logs 2 +}); + +a.set(2); // logs 4 +``` + + +### Using with Sapper + +At present, `Store` gets privileged treatment in [Sapper](https://sapper.svelte.technology) apps. A store instance can be created per-request, for example to contain user data. This store is attached to the component tree at render time, allowing `{$user.name}` to be server-rendered; its data is then passed to a client-side store. + +It's essential that this functionality be preserved. I'm not yet sure of the best way to achieve that. + +> Potential corner-cases to discuss: +> * What happens if a subscriber causes another subscriber to be removed (e.g. it results in a component subtree being destroyed)? +> * Is `$user.name` ambiguous (i.e. is `user` the observable, or `user.name`?) and if so how do we resolve the ambiguity +> * What happens if `$user` is declared in a scope that has an observable `user`? Do we just not subscribe? + + + +### Where does it go? + +This is (🐃) up for debate, but one possibility is that we put these functions in `svelte/state`: + +```js +import { observable, readOnlyObservable, derivedObservable } from 'svelte/state'; +``` + + +## How we teach this + +As with RFC 1, it's crucial that this be introduced with ample demos of how the `$` prefix works, in terms of the generated code. + +It's arguably simpler to teach than the existing store, since it's purely concerned with data, and avoids the 'magic' of auto-attaching. We do need to pick the right terminology though, and I'm open to alternatives to 'observable'. + + +## Drawbacks + +Like RFC 1, this is a breaking change, though RFC 1 will break existing stores anyway. The main reason not to pursue this option would be that the `$` prefix is overly magical, though I believe the convenience outweighs the modest learning curve. + +Another potential drawback is that anything that uses an observable (except the markup) must *itself* become observable; they are [red functions](http://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/). But this problem is presumably fundamental, rather than an avoidable consequence of the approach we've happened to choose. + + +## Alternatives + +* The existing store (but only at the top level; there is no opportunity to add stores at a lower level in the new design) +* Baking in an existing state management library (and its opinions) +* Using TC39 Observables +* Not having any kind of first-class treatment of reactive values, and relying on the reactive assignments mechanism exclusively + + +## Unresolved questions + +* What names to give everything +* The Sapper question +* The exact mechanics of how typechecking would work \ No newline at end of file From 2f274986a51c1fa619341280eb93c5d4cc8f6a6f Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 10 Nov 2018 22:45:24 -0500 Subject: [PATCH 2/8] typo --- text/0002-observables.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/text/0002-observables.md b/text/0002-observables.md index fd6df9d..d72ceb6 100644 --- a/text/0002-observables.md +++ b/text/0002-observables.md @@ -41,7 +41,7 @@ Essentially, the shift is from a single observable store of values to multiple o export default new Store({ user: { firstname: 'Fozzie', - lastname: 'Bear + lastname: 'Bear' }, volume: 0.5 }); @@ -52,7 +52,7 @@ export default new Store({ ```js export const user = observable({ firstname: 'Fozzie', - lastname: 'Bear + lastname: 'Bear' }); export const volume = observable(0.5); From cb09fb5f4aa1457784e2f9c65004923cab835f92 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 10 Nov 2018 22:46:12 -0500 Subject: [PATCH 3/8] link to PR --- text/0002-observables.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0002-observables.md b/text/0002-observables.md index d72ceb6..17c4e85 100644 --- a/text/0002-observables.md +++ b/text/0002-observables.md @@ -1,5 +1,5 @@ - Start Date: 2018-11-10 -- RFC PR: (leave this empty) +- RFC PR: [#5](https://github.com/sveltejs/rfcs/pull/5) - Svelte Issue: (leave this empty) # Svelte observables From f7420c74a50e87e4c02f83373cd234f1dc3258b4 Mon Sep 17 00:00:00 2001 From: Conduitry Date: Wed, 21 Nov 2018 08:47:34 -0500 Subject: [PATCH 4/8] Update text/0002-observables.md Co-Authored-By: Rich-Harris --- text/0002-observables.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/text/0002-observables.md b/text/0002-observables.md index 17c4e85..7761d52 100644 --- a/text/0002-observables.md +++ b/text/0002-observables.md @@ -300,7 +300,7 @@ Of course, some Observables *are* suitable for representing reactive values in a function adaptor(observable) { return { subscribe(fn) { - observable.subscribe({ + const subscriber = observable.subscribe({ next: fn }); @@ -469,4 +469,4 @@ Another potential drawback is that anything that uses an observable (except the * What names to give everything * The Sapper question -* The exact mechanics of how typechecking would work \ No newline at end of file +* The exact mechanics of how typechecking would work From c9288df47d3a8766640bc74682972f68621442bb Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Thu, 29 Nov 2018 08:44:50 -0500 Subject: [PATCH 5/8] rename to reactive stores. check out this nice smooth yak --- text/0002-observables.md | 139 ++++++++++++++++++--------------------- 1 file changed, 64 insertions(+), 75 deletions(-) diff --git a/text/0002-observables.md b/text/0002-observables.md index 7761d52..527e3e4 100644 --- a/text/0002-observables.md +++ b/text/0002-observables.md @@ -2,7 +2,7 @@ - RFC PR: [#5](https://github.com/sveltejs/rfcs/pull/5) - Svelte Issue: (leave this empty) -# Svelte observables +# Reactive stores ## Summary @@ -11,7 +11,7 @@ This RFC proposes a replacement for the existing [Store](https://svelte.technolo * Works with the Svelte 3 component design outlined in [RFC 1](https://github.com/sveltejs/rfcs/pull/4) * Allows any given component to subscribe to multiple sources of data from outside the component tree, rather than favouring a single app-level megastore * Typechecker friendliness -* Adapts to existing state management systems like [MobX](https://mobx.js.org/) or [TC39 Observables](https://github.com/tc39/proposal-observable), but does not require them +* Adapts to existing state management systems like [Redux](https://redux.js.org/) or [TC39 Observables](https://github.com/tc39/proposal-observable), but does not require them * Concise syntax that eliminates lifecycle boilerplate @@ -47,23 +47,21 @@ export default new Store({ }); ``` -...we have an `observable` (🐃) function: +...we create a new `writable` value store: ```js -export const user = observable({ +export const user = writable({ firstname: 'Fozzie', lastname: 'Bear' }); -export const volume = observable(0.5); +export const volume = writable(0.5); ``` -> The 🐃 emoji, used throughout this document, indicates a yak that needs shaving - Interested parties can read (and write) `user` without caring about `volume`, and vice versa: ```js -import { volume } from './observables.js'; +import { volume } from './stores.js'; const audio = document.querySelector('audio'); @@ -76,7 +74,7 @@ Inside a component's markup, a convenient shorthand sets up the necessary subscr ```js

Hello {$user.firstname}!

@@ -85,27 +83,27 @@ Inside a component's markup, a convenient shorthand sets up the necessary subscr This would compile to something like the following: ```js -import { ondestroy } from 'svelte'; -import { user } from './observables.js'; +import { onDestroy } from 'svelte'; +import { user } from './stores.js'; -const Component = defineComponent((__update) => { +function init($$self, $$make_dirty) { let $user; - ondestroy(user.subscribe(value => { + onDestroy(user.subscribe(value => { $user = value; - __update({ $user: true }); + $$makeDirty('$user'); })); - return () => ({ $user }); -}, create_main_fragment); + $$self.get = () => ({ $user }); +} ``` -### Observable API +### Store API -An observable *must* have a `subscribe` method, and it may also have additional methods like `set` and `update` if it isn't read-only: +A store *must* have a `subscribe` method, and it may also have additional methods like `set` and `update` if it isn't read-only: ```js -const number = observable(1); +const number = writable(1); const unsubscribe = number.subscribe(value => { console.log(`value is ${value}`); // logs 1 immediately @@ -123,7 +121,7 @@ number.set(4); // does nothing — unsubscribed An example implementation of this API: ```js -function observable(value) { +function writable(value) { const subscribers = []; function set(newValue) { @@ -151,9 +149,9 @@ function observable(value) { ``` -### Read-only observables +### Read-only stores -Some observables are read-only, created with `readOnlyObservable` (🐃): +Some stores are read-only, created with `readable`: ```js const unsubscribe = mousePosition.subscribe(pos => { @@ -166,7 +164,7 @@ mousePosition.set({ x: 100, y: 100 }); // Error: mousePosition.set is not a func An example implementation: ```js -function readOnlyObservable(start, value) { +function readable(start, value) { const subscribers = []; let stop; @@ -198,7 +196,7 @@ function readOnlyObservable(start, value) { } } -const mousePosition = readOnlyObservable(function start(set) { +const mousePosition = readable(function start(set) { function handler(event) { set({ x: event.clientX, @@ -213,16 +211,16 @@ const mousePosition = readOnlyObservable(function start(set) { }); ``` -### Derived observables +### Derived stores -An observable can be derived from other observables with `derivedObservable` (🐃): +A store can be derived from other stores with `derive` (🐃): ```js -const a = observable(1); -const b = observable(2); -const c = observable(3); +const a = writable(1); +const b = writable(2); +const c = writable(3); -const total = derivedObservable(a, b, c, (a, b, c) => a + b + c); +const total = derive([a, b, c], ([a, b, c]) => a + b + c); total.subscribe(value => { console.log(`total is ${value}`); // logs 'total is 6' @@ -234,14 +232,12 @@ c.set(4); // logs 'total is 7' Example implementation: ```js -function derivedObservable(...args) { - const fn = args.pop(); - +function derive(stores, fn) { return readOnlyObservable(set => { let inited = false; const values = []; - const unsubscribers = args.map((arg, i) => arg.subscribe(value => { + const unsubscribers = stores.map((store, i) => store.subscribe(value => { values[i] = value; if (inited) set(fn(...values)); })); @@ -258,22 +254,22 @@ function derivedObservable(...args) { > In the example above, `total` is recalculated immediately whenever the values of `a`, `b` or `c` are set. In some situations that's undesirable; you want to be able to set `a`, `b` *and* `c` without `total` being recalculated until you've finished. That could be done by putting the `set(fn(...values))` in a microtask, but that has drawbacks too. (Of course, that could be a decision left to the user.) Is this a fatal flaw in the design — should we strive for pull-based rather than push-based derived values? Or is it fine in reality? -Derived observables are, by nature, also read-only. They could be used, for example, to filter the items in a todo list: +Derived stores are, by nature, also read-only. They could be used, for example, to filter the items in a todo list: ```html @@ -285,12 +281,12 @@ Derived observables are, by nature, also read-only. They could be used, for exam ### Relationship with TC39 Observables -There is a [stage 1 proposal for an Observable object](https://github.com/tc39/proposal-observable) in JavaScript itself. It's quite different from the design documented here, which suggests we might need to come up with an alternative name. For now, I'll distinguish between an 'Observable' (TC39) and an 'observable' (us). +There is a [stage 1 proposal for an Observable object](https://github.com/tc39/proposal-observable) in JavaScript itself. Cards on the table: I'm not personally a fan of Observables. I've found them to be confusing and awkward to work with. But there are particular reasons why I don't think they're a good general solution for representing reactive values in a component: * They don't represent a single value changing over time, but rather a stream of distinct values. This is a subtle but important distinction -* Two different subscribers to the same observable could receive different values (!), where as in a UI you want two references to the same value to be guaranteed to be consistent +* Two different subscribers to the same Observable could receive different values (!), where as in a UI you want two references to the same value to be guaranteed to be consistent * Observables can 'complete', but declarative components (in Svelte and other frameworks) deliberately do not have a concept of time. The two things are incompatible * They have error-handling semantics that are very often redundant (what error could occur when observing the mouse position, for example?). When they're not redundant (e.g. in the case of data coming over the network), errors are perhaps best handled out-of-band, since the goal is to concisely represent the value in a component template @@ -309,10 +305,10 @@ function adaptor(observable) { } } -const tc39Observable = Observable.of('red', 'green', 'blue'); -const svelteObservable = adaptor(tc39Observable); +const observable = Observable.of('red', 'green', 'blue'); +const store = adaptor(observable); -const unsubscribe = svelteObservable.subscribe(color => { +const unsubscribe = store.subscribe(color => { console.log(color); // logs red, then green, then blue }); ``` @@ -329,7 +325,7 @@ More broadly, the same technique will work with existing state management librar // src/redux.js import { createStore } from 'redux'; -export const store = createStore((state = 0, action) => { +export const reduxStore = createStore((state = 0, action) => { switch (action.type) { case 'INCREMENT': return state + 1 @@ -340,27 +336,27 @@ export const store = createStore((state = 0, action) => { } }); -function adaptor(store) { +function adaptor(reduxStore) { return { subscribe(fn) { - return store.subscribe(() => { - fn(store.getState()); + return reduxStore.subscribe(() => { + fn(reduxStore.getState()); }); } }; } -export const observable = adaptor(store); +export const store = adaptor(reduxStore); ``` ```html - ``` @@ -368,18 +364,19 @@ export const observable = adaptor(store); #### Immer ```js +import { writable } from 'svelte/store.js'; import { produce } from 'immer'; function immerObservable(data) { - const o = observable(data); + const store = writable(data); function update(fn) { - o.update(state => produce(state, fn)); + store.update(state => produce(state, fn)); } return { update, - subscribe: o.subscribe + subscribe: store.subscribe }; } @@ -398,22 +395,23 @@ todos.update(draft => { #### Shiz ```js +import { readable } from 'svelte/store.js'; import { value, computed } from 'shiz'; const a = value(1); const b = computed([a], ([a]) => a * 2); function shizObservable(shiz) { - return readOnlyObservable(function start(set) { + return readable(function start(set) { return shiz.on('change', () => { set(shiz.get()); }); }, shiz.get()); } -const observable = shizObservable(b); +const store = shizObservable(b); -const unsubscribe = observable.subscribe(value => { +const unsubscribe = store.subscribe(value => { console.log(value); // logs 2 }); @@ -425,36 +423,28 @@ a.set(2); // logs 4 At present, `Store` gets privileged treatment in [Sapper](https://sapper.svelte.technology) apps. A store instance can be created per-request, for example to contain user data. This store is attached to the component tree at render time, allowing `{$user.name}` to be server-rendered; its data is then passed to a client-side store. -It's essential that this functionality be preserved. I'm not yet sure of the best way to achieve that. +It's essential that this functionality be preserved. I'm not yet sure of the best way to achieve that. The most promising suggestion is that we use regular props instead, passed into the top-level component. (In some ways this would be more ergonomic, since the user would no longer be responsible for setting up the store client-side.) > Potential corner-cases to discuss: > * What happens if a subscriber causes another subscriber to be removed (e.g. it results in a component subtree being destroyed)? -> * Is `$user.name` ambiguous (i.e. is `user` the observable, or `user.name`?) and if so how do we resolve the ambiguity -> * What happens if `$user` is declared in a scope that has an observable `user`? Do we just not subscribe? - - - -### Where does it go? - -This is (🐃) up for debate, but one possibility is that we put these functions in `svelte/state`: - -```js -import { observable, readOnlyObservable, derivedObservable } from 'svelte/state'; -``` +> * Is `$user.name` ambiguous (i.e. is `user` the store, or `user.name`?) and if so how do we resolve the ambiguity +> * What happens if `$user` is declared in a scope that has a store `user`? Do we just not subscribe? ## How we teach this As with RFC 1, it's crucial that this be introduced with ample demos of how the `$` prefix works, in terms of the generated code. -It's arguably simpler to teach than the existing store, since it's purely concerned with data, and avoids the 'magic' of auto-attaching. We do need to pick the right terminology though, and I'm open to alternatives to 'observable'. +It's arguably simpler to teach than the existing store, since it's purely concerned with data, and avoids the 'magic' of auto-attaching. ## Drawbacks Like RFC 1, this is a breaking change, though RFC 1 will break existing stores anyway. The main reason not to pursue this option would be that the `$` prefix is overly magical, though I believe the convenience outweighs the modest learning curve. -Another potential drawback is that anything that uses an observable (except the markup) must *itself* become observable; they are [red functions](http://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/). But this problem is presumably fundamental, rather than an avoidable consequence of the approach we've happened to choose. +Another potential drawback is that anything that uses a store (except the markup) must *itself* become a reactive store; they are [red functions](http://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/). But this problem is presumably fundamental, rather than an avoidable consequence of the approach we've happened to choose. + +> [RFC 3](https://github.com/sveltejs/rfcs/blob/reactive-declarations/text/0003-reactive-declarations.md) presents an escape hatch to this problem ## Alternatives @@ -467,6 +457,5 @@ Another potential drawback is that anything that uses an observable (except the ## Unresolved questions -* What names to give everything * The Sapper question * The exact mechanics of how typechecking would work From 3ec57e010fa3364ff7ef2cc84f7ddad16fe3936b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sun, 16 Dec 2018 15:46:04 -0500 Subject: [PATCH 6/8] update to use current Svelte 3 implementations --- text/0002-observables.md | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/text/0002-observables.md b/text/0002-observables.md index 527e3e4..ca34efb 100644 --- a/text/0002-observables.md +++ b/text/0002-observables.md @@ -138,10 +138,10 @@ function writable(value) { subscribers.push(fn); fn(value); - return function() { + return () => { const index = subscribers.indexOf(fn); if (index !== -1) subscribers.splice(index, 1); - } + }; } return { set, update, subscribe }; @@ -188,12 +188,12 @@ function readable(start, value) { if (index !== -1) subscribers.splice(index, 1); if (subscribers.length === 0) { - stop(); + stop && stop(); stop = null; } }; } - } + }; } const mousePosition = readable(function start(set) { @@ -213,7 +213,7 @@ const mousePosition = readable(function start(set) { ### Derived stores -A store can be derived from other stores with `derive` (🐃): +A store can be derived from other stores with `derive`: ```js const a = writable(1); @@ -233,20 +233,30 @@ Example implementation: ```js function derive(stores, fn) { - return readOnlyObservable(set => { + const single = !Array.isArray(stores); + if (single) stores = [stores]; + + const auto = fn.length === 1; + + return readable(set => { let inited = false; const values = []; + const sync = () => { + const result = fn(single ? values[0] : values, set); + if (auto) set(result); + } + const unsubscribers = stores.map((store, i) => store.subscribe(value => { values[i] = value; - if (inited) set(fn(...values)); + if (inited) sync(); })); inited = true; - set(fn(...values)); + sync(); return function stop() { - unsubscribers.forEach(fn => fn()); + run_all(unsubscribers); }; }); } From 261a2f129ca787e20082f6371556477bf38c6e96 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 21 Dec 2018 16:26:05 -0500 Subject: [PATCH 7/8] tidy up example code --- text/0002-observables.md | 48 ++++++++++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/text/0002-observables.md b/text/0002-observables.md index ca34efb..bfaf9b4 100644 --- a/text/0002-observables.md +++ b/text/0002-observables.md @@ -86,11 +86,11 @@ This would compile to something like the following: import { onDestroy } from 'svelte'; import { user } from './stores.js'; -function init($$self, $$make_dirty) { +function init($$self, $$invalidate) { let $user; onDestroy(user.subscribe(value => { $user = value; - $$makeDirty('$user'); + $$invalidate('$user', $user); })); $$self.get = () => ({ $user }); @@ -127,19 +127,21 @@ function writable(value) { function set(newValue) { if (newValue === value) return; value = newValue; - subscribers.forEach(fn => fn(value)); + subscribers.forEach(s => s[1]()); + subscribers.forEach(s => s[0](value)); } function update(fn) { set(fn(value)); } - function subscribe(fn) { - subscribers.push(fn); - fn(value); + function subscribe(run, invalidate = noop) { + const subscriber = [run, invalidate]; + subscribers.push(subscriber); + run(value); return () => { - const index = subscribers.indexOf(fn); + const index = subscribers.indexOf(subscriber); if (index !== -1) subscribers.splice(index, 1); }; } @@ -171,20 +173,22 @@ function readable(start, value) { function set(newValue) { if (newValue === value) return; value = newValue; - subscribers.forEach(fn => fn(value)); + subscribers.forEach(s => s[1]()); + subscribers.forEach(s => s[0](value)); } return { - subscribe(fn) { + subscribe(run, invalidate = noop) { if (subscribers.length === 0) { stop = start(set); } - subscribers.push(fn); - fn(value); + const subscriber = [run, invalidate]; + subscribers.push(subscriber); + run(value); return function() { - const index = subscribers.indexOf(fn); + const index = subscribers.indexOf(subscriber); if (index !== -1) subscribers.splice(index, 1); if (subscribers.length === 0) { @@ -237,20 +241,30 @@ function derive(stores, fn) { if (single) stores = [stores]; const auto = fn.length === 1; + let value = {}; return readable(set => { let inited = false; const values = []; + let pending = 0; + const sync = () => { + if (pending) return; const result = fn(single ? values[0] : values, set); - if (auto) set(result); + if (auto && (value !== (value = result))) set(result); } - const unsubscribers = stores.map((store, i) => store.subscribe(value => { - values[i] = value; - if (inited) sync(); - })); + const unsubscribers = stores.map((store, i) => store.subscribe( + value => { + values[i] = value; + pending &= ~(1 << i); + if (inited) sync(); + }, + () => { + pending |= (1 << i); + }) + ); inited = true; sync(); From 96d913d6940fa6a6e934a792525745890fcb5c63 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 28 Dec 2018 13:24:03 -0500 Subject: [PATCH 8/8] Rename 0002-observables.md to 0002-reactive-stores.md --- text/{0002-observables.md => 0002-reactive-stores.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename text/{0002-observables.md => 0002-reactive-stores.md} (100%) diff --git a/text/0002-observables.md b/text/0002-reactive-stores.md similarity index 100% rename from text/0002-observables.md rename to text/0002-reactive-stores.md