From afa4be53591a0a411d712e098560f62b9a864cda Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 2 Nov 2018 00:08:34 -0400 Subject: [PATCH 01/34] make a start on reactive assignments RFC --- text/0000-reactive-assignments.md | 464 ++++++++++++++++++++++++++++++ 1 file changed, 464 insertions(+) create mode 100644 text/0000-reactive-assignments.md diff --git a/text/0000-reactive-assignments.md b/text/0000-reactive-assignments.md new file mode 100644 index 0000000..837d4e6 --- /dev/null +++ b/text/0000-reactive-assignments.md @@ -0,0 +1,464 @@ +- Start Date: 2018-11-01 +- RFC PR: (leave this empty) +- Svelte Issue: (leave this empty) + +# Reactive assignments + +## Summary + +This RFC proposes a radically new component design that solves the biggest problems with Svelte in its current incarnation, resulting in smaller apps and less code to write. It does so by leveraging Svelte's unique position as a compiler to solve the problem of *reactivity* at the language level. + +**This RFC is a work-in-progress, and some pieces are missing!** + +## Motivation + +Many developers report being intrigued by Svelte, but unwilling to accommodate some of the quirks of its design. For example, using features like nested components or transitions requires that you *register* them, involving unfortunate boilerplate: + +```html +
+ +
+ + +``` + +Declaring methods is also somewhat cumbersome, and in many editor configurations leads to red squigglies since it violates the normal semantics of JavaScript: + +```html + + + +``` + +Perhaps the biggest hangup is that the markup doesn't have access to arbitrary values inside the ` +``` + +There are legitimate technical and historical reasons for this design. And some people — particularly those with a background in Vue or Ractive — are familiar with it and enjoy it. But the reality is that for most of us, it sucks. It would be much nicer to do something like this: + +```html + + +

Hello {capitalise(name)}!

+

Your name has {len()} letters in it.

+``` + +Why can't we? Because of *reactivity*. There's no way for the view to 'know' if `name` has changed. + + +## The reactivity problem + +UI frameworks are all, to some degree, examples of reactive programming. The notion is that your view can be thought of as a function of your state, and so when your state changes the view must, well... react. + +Reactivity is implemented in a variety of different ways (note: I am not expert in these frameworks other than Svelte, please correct any misapprehensions): + +* **React**'s approach is to re-render the world on every state change. This necessitates the use of a virtual DOM, which in turn necessitates a virtual DOM reconciler. It is effective, but an inefficient approach for three reasons: first, the developer's code must run frequently, providing ample opportunity for 'death by a thousand paper cuts' particularly if the developer is unskilled; second, the virtual DOM itself is a short-lived object, thirdly, discovering 'what changed' via virtual DOM diffing is wasteful in the common case that the answer is 'not much'. Reasonable people can (and do) disagree about whether these inefficiences are impactful on real apps (the answer is presumably 'it depends'), but it is well known that the most reliable way to make code faster is for it to do less work, and React does a lot of work. + +* **Vue** can track which values changed using getters and setters (accessors). In other words, your data forms the basis of a 'viewmodel', with accessors corresponding to the initial properties. Assigning to one of them — `vm.foo = 1` — schedules an update. This code is pleasant to write but is not without its downsides — it can result in unpredictable behaviour as values are passed around the app, and it encourages mutation. It also involves some minor computational and memory overhead, and some gotchas when setting as-yet-unknown properties (this will be fixed in Vue 3 with proxies, but presumably at the cost of IE support). + +* **Svelte** provides each component with a `set` method (and a corresponding `get` method) allowing the developer to make explicit state changes. (Those changes are fully synchronous, which is both good and bad — good because the mental model and resulting stack traces are simple, bad because it can result in layout thrashing, buggy lifecycles, and infinite loops that need to be guarded against.) This is verbose when mutating or copying objects, and puts valuable things like first-class TypeScript integration essentially out of reach. + +None of these are ideal. But [we're a compiler](https://mobile.twitter.com/Rich_Harris/status/1057290365395451905), which means we have tools in our toolbox that are unique to Svelte. What if we could solve reactivity *at the language level*? + +This proposal outlines a novel (as far as we're aware) solution, while bringing a number of significant benefits: + +* Easier-to-grok design +* Less application code +* Smaller bundles +* Simpler, more robust lifecycles +* A realistic path towards first-class TypeScript support +* Well-defined contracts between components and their consumers +* Opportunities to adopt ideas like Suspense + + +## Detailed design + +Consider a counter component, the workhorse of examples such as these: + +```html + + + +``` + +> 🐃 For familiarity, we use the current event listener syntax. It may be preferable to use `{() => count += 1}` — a large part of the reason we avoid that currently is because `this` makes it tricky, which is no longer an issue. + +> (The 🐃 emoji used throughout this document indicates a yak that needs shaving.) + +Here, we have declared a single state variable, `count`, with an initial value of `0`. The code inside the ` + + +``` + +...would be transformed to something like this: + +```js +function create_main_fragment(component, ctx) { + // ... the code that creates and maintains the view — + // this will be relatively unaffected by this proposal +} + +const Component = defineComponent((__update) => { + let count = 0; + const incr = () => { + count += 1; + __update({ count: true }); + }; + + // this creates the top-level `ctx` variable for `create_main_fragment` + return () => { count }; +}, create_main_fragment); +``` + +**This behaviour might seem surprising, even shocking at first.** Variable assignment does not have side-effects in JavaScript, meaning that this is arguably something else — [SvelteScript](https://mobile.twitter.com/youyuxi/status/1057291776724271104), perhaps. In practice though, developers readily embrace 'magic' that makes their day-to-day lives easier as long as the mechanisms are ultimately easy to understand — something observed with React Hooks, Vue's reactivity system, Immer's approach to immutable data, and countless other examples. This does underscore the need for this code transformation to be well-documented and explained, however. + + +### Props + +Many frameworks (and also web components) have a conceptual distinction between 'props' (values passed *to* a component) and 'state' (values that are internal to the component). Svelte does not. This is a shortcoming. + +The proposed solution is to use the `export` keyword to declare a value that can be set from outside as a prop: + +```html + + +

Hello {name}!

+``` + +This would compile to something like the following: + +```js +const Component = defineComponent((__update, __props) => { + let name = 'world'; + + __props((changed, values) => { + name = values.name; + __update(changed); + }); + + // this creates the top-level `ctx` variable for `create_main_fragment` + return () => { name }; +}, create_main_fragment); +``` + +**This is an abuse of syntax**, but no worse than Svelte's existing crime of abusing `export default`. As long as it is well-communicated, it is easy to understand. Using existing syntax, rather than inventing our own, allows us to take full advantage of existing tooling and editor support. + +To summarise, there are **3 simple rules** for understanding the code in a Svelte component's ` + +

The time is {format_time(time)}

+``` + +The `ondestroy` callback is associated with the component instance because it is called during instantiation; no compiler magic is involved. Beyond that (if it is called outside instantiation, an error is thrown), there are no rules or restrictions as to when a lifecycle function is called. Reusability is therefore straightforward: + +```html + + +

The time is {format_time(time)}

+``` + +```js +// helpers.js +import { ondestroy } from 'svelte'; + +export function use_interval(fn, ms) { + const interval = setInterval(fn, ms); + ondestroy(() => clearInterval(interval)); + fn(); +} +``` + +> This might seem less ergonomic than React Hooks, whereby you can do `const time = useCustomHook()`. The payoff is that you don't need to run that code on every single state change. + +There are two other lifecycle functions required — (🐃) `onstate` and (🐃) `onupdate`: + +```html + +``` + +> Note that the `changed`, `current` and `previous` arguments, present in Svelte 2, are absent from these callbacks — I believe they are now unnecessary. + +Currently, `onstate` and `onupdate` run before and after every state change. Because Svelte 2 doesn't differentiate between external props and internal state, this can easily result in confusing cyclicality: + +```js + +``` + +Under this proposal, the `onstate` callback runs whenever props change but *not* when private state changes. This makes cycles impossible: + +```html + +``` + +> Note that `previous_temperature`, if unused in the view, will not get the reactive treatment. + +Any `onupdate` callbacks would run after the view was updated, whether as a result of prop or state changes. Assignments in an `onupdate` callback would result in a synchronous re-render but would *not* cause the callback to run again. This would allow components to respond to layout changes, for example. + +A special `onupdate` case is that you often need to do work a single time once the component is mounted. This could be done in userland... + +```js +onupdate(once(() => { + doInitialSetup(); +})); +``` + +...but it may turn out to be better to have a dedicated function for that. + + +### Refs + +TODO (including component refs) + +### Store + +TODO + +### Spread props + +TODO + +### namespace/tag options + +TODO + +### Events + +TODO + +### Component bindings + +TODO + +### Component API + +TODO + +### Preload + +TODO + +### Server-side rendering + +TODO + +### Standalone components + +TODO + +### Sync vs async rendering + +TODO + +### Suspense + +TODO + +### TypeScript + +TODO + +### Custom elements + +TODO + +### Dependency tracking + +TODO + +### Examples + +TODO (show how to do equivalent of now-missing things like computed properties) + + + +## How we teach this + +> What names and terminology work best for these concepts and why? How is this +idea best presented? As a continuation of existing Svelte patterns, or as a +wholly new one? + +> Would the acceptance of this proposal mean the Svelte guides must be +re-organized or altered? Does it change how Svelte is taught to new users +at any level? + +> How should this feature be introduced and taught to existing Svelte +users? + +## Drawbacks + +> Why should we *not* do this? Please consider the impact on teaching Svelte, +on the integration of this feature with other existing and planned features, +on the impact of the API churn on existing apps, etc. + +> There are tradeoffs to choosing any path, please attempt to identify them here. + +## Alternatives + +> What other designs have been considered? What is the impact of not doing this? + +> This section could also include prior art, that is, how other frameworks in the same domain have solved this problem. + +## Unresolved questions + +> Optional, but suggested for first drafts. What parts of the design are still +TBD? \ No newline at end of file From fd091a65eff8056ee57f214f19195b7bb757396d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 2 Nov 2018 00:14:05 -0400 Subject: [PATCH 02/34] typo --- text/0000-reactive-assignments.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/text/0000-reactive-assignments.md b/text/0000-reactive-assignments.md index 837d4e6..13a548e 100644 --- a/text/0000-reactive-assignments.md +++ b/text/0000-reactive-assignments.md @@ -38,9 +38,11 @@ Declaring methods is also somewhat cumbersome, and in many editor configurations From 0c10cd6da8d069f6700f9eb5d011a72eac1b7a75 Mon Sep 17 00:00:00 2001 From: Conduitry Date: Fri, 2 Nov 2018 07:06:42 -0400 Subject: [PATCH 03/34] Update text/0000-reactive-assignments.md Co-Authored-By: Rich-Harris --- text/0000-reactive-assignments.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/text/0000-reactive-assignments.md b/text/0000-reactive-assignments.md index 13a548e..39714d3 100644 --- a/text/0000-reactive-assignments.md +++ b/text/0000-reactive-assignments.md @@ -89,7 +89,7 @@ There are legitimate technical and historical reasons for this design. And some

Hello {capitalise(name)}!

@@ -463,4 +463,4 @@ on the impact of the API churn on existing apps, etc. ## Unresolved questions > Optional, but suggested for first drafts. What parts of the design are still -TBD? \ No newline at end of file +TBD? From 2b2ff98d26ee2663ec2ceeba9ddf502109812743 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 2 Nov 2018 08:00:41 -0400 Subject: [PATCH 04/34] clarify oncreate stuff --- text/0000-reactive-assignments.md | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/text/0000-reactive-assignments.md b/text/0000-reactive-assignments.md index 39714d3..0e068c0 100644 --- a/text/0000-reactive-assignments.md +++ b/text/0000-reactive-assignments.md @@ -236,7 +236,9 @@ To summarise, there are **3 simple rules** for understanding the code in a Svelt Many components need to respond to *lifecycle events*. These are currently expressed in Svelte via four [lifecycle hooks](https://svelte.technology/guide#lifecycle-hooks) — `onstate`, `oncreate`, `onupdate` and `ondestroy`. -The `oncreate` hook is redundant, since the ` +``` + +This could compile to something like the following: + +```js +import { onupdate } from 'svelte'; + +const Component = defineComponent((__update, __props, __refs) => { + let canvas; + let ctx; + + onupdate(() => { + if (!ctx) ctx = canvas.getContext('2d'); + draw_some_shapes(ctx); + }); + + __refs(refs => { + canvas = refs.canvas; + }); + + return () => {}; +}, create_main_fragment); +``` + +The same would apply to component refs. + ### Store @@ -393,7 +438,21 @@ TODO ### namespace/tag options -TODO +The default export from a Svelte 2 component can include compiler options — for example declaring a namespace (for SVG components) or a tag name (for custom elements): + +```js +export default { + namespace: 'svg', + tag: 'my-cool-thing +}; +``` + +Under this proposal, these options would be expressed by a new `` tag: + +```html + +``` + ### Events From 6e12409b220f100c1785bcc3f619e0dee98a4161 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 2 Nov 2018 08:21:14 -0400 Subject: [PATCH 06/34] typo --- text/0000-reactive-assignments.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/text/0000-reactive-assignments.md b/text/0000-reactive-assignments.md index 0f0b27c..ea8d202 100644 --- a/text/0000-reactive-assignments.md +++ b/text/0000-reactive-assignments.md @@ -443,7 +443,7 @@ The default export from a Svelte 2 component can include compiler options — fo ```js export default { namespace: 'svg', - tag: 'my-cool-thing + tag: 'my-cool-thing' }; ``` From db1c0f7219c804e6fc7d1ec0b948f56c799bd3cb Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 2 Nov 2018 09:25:08 -0400 Subject: [PATCH 07/34] events --- text/0000-reactive-assignments.md | 38 ++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/text/0000-reactive-assignments.md b/text/0000-reactive-assignments.md index ea8d202..bd7c9ef 100644 --- a/text/0000-reactive-assignments.md +++ b/text/0000-reactive-assignments.md @@ -456,7 +456,43 @@ Under this proposal, these options would be expressed by a new `` t ### Events -TODO +Svelte 2 components can fire events with `this.fire(eventName, optionalData)`. These events can be listened to programmatically, with `component.on(eventName, callback)`, or declaratively in component markup: + +```html + +``` + +Since there's no more `this`, there's no more `this.fire`, which means we need to rethink events. This gives us an opportunity to align with web components, where `CustomEvent` is used ([issue here](https://github.com/sveltejs/svelte/issues/1655)) — this also gives us event bubbling, which avoids needing to manually propagate events. + +The high-level proposal is to introduce a function called `createEventDispatcher` (🐃) that would return a function for dispatching events: + +```js +import { createEventDispatcher } from 'svelte'; + +const dispatch = createEventDispatcher(); + +let i = 0; +const interval = setInterval(() => { + dispatch(i++ % 2 ? 'tick' : 'tock', { + answer: 42 + }); +}, 1000); +``` + +This would create a `CustomEvent` with an `event.type` of `tick` (or `tock`) and an `event.detail` of `{ answer: 42 }`. + +> 🐃 We could match the arguments to `new CustomEvent(name, opts)` instead — where `detail` is one of the options passed to the constructor alongside things like `bubbles` and `cancelable` + +As with lifecycle functions, the `dispatch` is bound to the component because of when `createEventDispatcher` is called. + +Listening to events is currently done like so: + +```html + +``` + +This effectively becomes `{() => this.doSomething()}`. The shorthand is nice, but it is slightly confusing (the context is implicit, and the right-hand side must be a CallExpression which limits flexibility, although there's an [issue for that](https://github.com/sveltejs/svelte/issues/1766)). In the new model, there are no longer any problems around `this`, so it probably makes sense to allow arbitrary expressions instead. + ### Component bindings From 6a9e4183977fbcf15a75e4e97d8dba852a3bd3c1 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 2 Nov 2018 09:42:04 -0400 Subject: [PATCH 08/34] component API --- text/0000-reactive-assignments.md | 62 ++++++++++++++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/text/0000-reactive-assignments.md b/text/0000-reactive-assignments.md index bd7c9ef..3f1d00e 100644 --- a/text/0000-reactive-assignments.md +++ b/text/0000-reactive-assignments.md @@ -453,6 +453,10 @@ Under this proposal, these options would be expressed by a new `` t ``` +### Script-less components + +TODO + ### Events @@ -500,7 +504,63 @@ TODO ### Component API -TODO +Under this proposal, there is no longer a `this` with state and methods *inside* a component. But there needs to be a way to interact with the component from the outside world. + +Instantiating a top-level component probably needn't change (except 🐃 maybe changing `data` to `props`): + +```js +import App from './App.html'; + +const app = new App({ + target: document.querySelector('body'), + props: { + name: 'world' + } +}); +``` + +Exported properties can be exposed as accessors: + +```js +app.name; // world +app.name = 'everybody'; triggers a (sync?) update +``` + +This creates consistent behaviour between Svelte components that are compiled to custom elements, and those that are not, while also making it easy to understand the component's contract. + +Of the five **built-in methods** that currently comprise the [component API](https://svelte.technology/guide#component-api) — `get`, `set`, `fire`, `on` and `destroy` — we no longer need the first three. `on` and `destroy` are still necessary. + +In some cases, a component that is designed to be used as a standalone widget will create its own **custom methods**. In Svelte 2, these are lumped in with 'private' (except not really) methods. Under this proposal, custom methods are just exported variables that happen to be functions: + +```html + + +
+ {#if visible} +

now you see me

+ {/if} +
+``` + +```js +import Peekaboo from './Peekaboo.html'; + +const peekaboo = new Peekaboo(...); +peekaboo.show(); +``` + +An `export const` would be a signal to the compiler that a property is read-only — in other words, attempting to assign to it would cause an error. + ### Preload From a7742a8715d76eb35e01e049ff044797d3047fb6 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 2 Nov 2018 09:46:04 -0400 Subject: [PATCH 09/34] script-less components --- text/0000-reactive-assignments.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/text/0000-reactive-assignments.md b/text/0000-reactive-assignments.md index 3f1d00e..622f724 100644 --- a/text/0000-reactive-assignments.md +++ b/text/0000-reactive-assignments.md @@ -455,7 +455,21 @@ Under this proposal, these options would be expressed by a new `` t ### Script-less components -TODO +At present it is possible to create components with no ` + +

Hello {name}!

+``` ### Events From 130d1b630dc14804619e2f3769dc3732e694c629 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 2 Nov 2018 10:03:39 -0400 Subject: [PATCH 10/34] typescript --- text/0000-reactive-assignments.md | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/text/0000-reactive-assignments.md b/text/0000-reactive-assignments.md index 622f724..003e193 100644 --- a/text/0000-reactive-assignments.md +++ b/text/0000-reactive-assignments.md @@ -598,7 +598,20 @@ TODO ### TypeScript -TODO +A major advantage of this proposal over Svelte 2 is that components become far more amenable to typechecking and other forms of static analysis. Even without TypeScript, VSCode is able to offer much richer feedback now than with Svelte 2. + +An eventual goal is to be able to use TypeScript in components: + +```html + + +

Hello {name}!

+``` + +Editor integrations would ideally offer autocompletion and as-you-type typechecking inside the markup. This is not my area of expertise, however, so I would welcome feedback on this proposal from people who are more familiar with the TypeScript compiler API and ecosystem. + ### Custom elements From ce0c82fbcd15f75fc3d5df89c1ff04315a7832a6 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 2 Nov 2018 10:06:14 -0400 Subject: [PATCH 11/34] custom elements --- text/0000-reactive-assignments.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/text/0000-reactive-assignments.md b/text/0000-reactive-assignments.md index 003e193..7908b8d 100644 --- a/text/0000-reactive-assignments.md +++ b/text/0000-reactive-assignments.md @@ -615,7 +615,10 @@ Editor integrations would ideally offer autocompletion and as-you-type typecheck ### Custom elements -TODO +Svelte would continue to offer a custom element compile target. No real changes would be involved here, since this proposal brings 'vanilla' Svelte components in line with web components viz. property access and event handling. + +The `props` compiler option is redundant now that the contract is defined via `export`ed variables. The `tag` option can be set via `` as discussed above. + ### Dependency tracking From 15c8a79f86e7738c30521000298e1e6b9e2810b9 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 2 Nov 2018 10:17:06 -0400 Subject: [PATCH 12/34] dependency tracking --- text/0000-reactive-assignments.md | 65 ++++++++++++++++++++++++++++++- 1 file changed, 64 insertions(+), 1 deletion(-) diff --git a/text/0000-reactive-assignments.md b/text/0000-reactive-assignments.md index 7908b8d..c547700 100644 --- a/text/0000-reactive-assignments.md +++ b/text/0000-reactive-assignments.md @@ -622,7 +622,70 @@ The `props` compiler option is redundant now that the contract is defined via `e ### Dependency tracking -TODO +In Svelte 2, 'computed properties' use compile-time dependency tracking to derive values from state, avoiding recomputation when dependencies haven't changed. These computed properties are 'push-based' rather than 'pull-based', which can result in unnecessary work: + +```html +{#if visible} +

{bar}

+{/if} + + +``` + +In the example above, there is no need to calculate `bar` since it is not rendered. + +Under this proposal, there is no longer a separate concept of computed properties. Instead, we can just use functions: + +```html + + +{#if visible} +

{bar()}

+{/if} +``` + +Note that we now *invoke* `bar` in the markup. If `foo` were to change while `visible` were true, we would need to update the contents of `

`. That means we need to track the values that could affect the return value, resulting in compiled code similar to this: + +```js +function update(changed, ctx) { + if (changed.foo) p.textContent = ctx.bar(); +} +``` + +In some situations dependency tracking may be impossible — I'm not sure. In those cases, we can simply bail out: + +```js +function update(changed, ctx) { + p.textContent = ctx.bar(); +} +``` + +Note that this system is now pull-based rather than push-based. One drawback over the current system of computed properties is that values that are referenced multiple times will also be calculated multiple times: + +```html +{#if visible} +

{bar()} — {bar()}

+{/if} +``` + +Potentially, this could be alleviated with compiler magic (i.e. detecting multiple invocations of pure functions with the same arguments, and computing once before rendering) or userland memoization, but it warrants further exploration. + ### Examples From 3e3758c30d0628b667047a7a4e376a9d9b4f4962 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 2 Nov 2018 10:42:02 -0400 Subject: [PATCH 13/34] bindings --- text/0000-reactive-assignments.md | 40 +++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/text/0000-reactive-assignments.md b/text/0000-reactive-assignments.md index c547700..b0784e1 100644 --- a/text/0000-reactive-assignments.md +++ b/text/0000-reactive-assignments.md @@ -512,9 +512,40 @@ Listening to events is currently done like so: This effectively becomes `{() => this.doSomething()}`. The shorthand is nice, but it is slightly confusing (the context is implicit, and the right-hand side must be a CallExpression which limits flexibility, although there's an [issue for that](https://github.com/sveltejs/svelte/issues/1766)). In the new model, there are no longer any problems around `this`, so it probably makes sense to allow arbitrary expressions instead. -### Component bindings +### Bindings + +Element bindings are a convenient shorthand, which I believe we should preserve: + +```html + + + + + + + +``` + +Component bindings are less clear-cut. In the past they've been a source of confusion for the same reason that the lifecycle hook dance causes confusion upon app initialisation. It's possible that the new lifecycle concepts will eliminate those headaches, but this requires further exploration. + +The reason we can *consider* removing component bindings is that it's now trivial to pass state-altering functions down to components: + +```html + + + + the mouse is at {mouse.x},{mouse.y} + +``` -TODO ### Component API @@ -687,9 +718,10 @@ Note that this system is now pull-based rather than push-based. One drawback ove Potentially, this could be alleviated with compiler magic (i.e. detecting multiple invocations of pure functions with the same arguments, and computing once before rendering) or userland memoization, but it warrants further exploration. -### Examples +### svelte-extras -TODO (show how to do equivalent of now-missing things like computed properties) + +### Examples From 5af55cb653766dd6e0c401c31f76701e935168fb Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 2 Nov 2018 10:55:24 -0400 Subject: [PATCH 14/34] preload and setup --- text/0000-reactive-assignments.md | 41 +++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/text/0000-reactive-assignments.md b/text/0000-reactive-assignments.md index b0784e1..a72d3b3 100644 --- a/text/0000-reactive-assignments.md +++ b/text/0000-reactive-assignments.md @@ -607,9 +607,46 @@ peekaboo.show(); An `export const` would be a signal to the compiler that a property is read-only — in other words, attempting to assign to it would cause an error. -### Preload +### `preload` and `setup` + +There is a rather awkward mechanism for declaring static properties on a component constructor in Svelte 2 — the `setup` hook: + +```html + +``` + +This is deeply weird, and due to an oversight (that we can't correct without a breaking change) only runs on the client. + +Since Sapper requires that components have some way of declaring their data dependencies prior to rendering, and since `setup` is so cumbersome, there is a special case made for `preload`. The `preload` function is attached to components on both client and server, and has no well-defined behaviour; it is purely convention. + +We can do better, **but it requires something potentially controversial** — a second ` + + + +{#each things as thing} +

{thing}

+{/each} +``` + +`things` would be injected into the instance ` + +

the list is {listHeight}px tall:

+ +
    + {#each items as item} +
  • {item}
  • + {/each} +
+``` + +...then you want the change to `listHeight` to be reflected in the view as soon as all the `onupdate` callbacks have fired, *not* after an animation frame (which would result in lag). + +Similarly, in terms of the public component API, it might be necessary for `customElement.foo = 1` to result in a synchronous update. + ### Suspense From 78f2c7c5c1445f62afab2c404a982ba517b2fd3b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 2 Nov 2018 11:34:26 -0400 Subject: [PATCH 18/34] suspense --- text/0000-reactive-assignments.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/text/0000-reactive-assignments.md b/text/0000-reactive-assignments.md index a5fa7a3..57d3e55 100644 --- a/text/0000-reactive-assignments.md +++ b/text/0000-reactive-assignments.md @@ -712,7 +712,8 @@ Similarly, in terms of the public component API, it might be necessary for `cust ### Suspense -TODO +This proposal isn't directly concerned with [implementing Suspense](https://github.com/sveltejs/svelte/issues/1736), but care should be taken to ensure that Suspense can be implemented atop this proposal without breaking changes. + ### TypeScript From 33ce75acd4001a4e9dbd73f4bcb289f66da90a6a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 2 Nov 2018 11:58:59 -0400 Subject: [PATCH 19/34] store, spread props --- text/0000-reactive-assignments.md | 78 +++++++++++++++++++++++++++---- 1 file changed, 70 insertions(+), 8 deletions(-) diff --git a/text/0000-reactive-assignments.md b/text/0000-reactive-assignments.md index 57d3e55..4684952 100644 --- a/text/0000-reactive-assignments.md +++ b/text/0000-reactive-assignments.md @@ -428,14 +428,6 @@ const Component = defineComponent((__update, __props, __refs) => { The same would apply to component refs. -### Store - -TODO - -### Spread props - -TODO - ### namespace/tag options The default export from a Svelte 2 component can include compiler options — for example declaring a namespace (for SVG components) or a tag name (for custom elements): @@ -666,6 +658,73 @@ It's not clear how this proposal affects standalone components (as opposed to th One consequence of the new way of doing things: Svelte no longer lives in `pkg.devDependencies`, but would instead need to become a `dependency`. Not very On Brand but probably a small price to pay. +### Store + +A [store](https://svelte.technology/guide#state-management) can be attached to a component, and it will be passed down to all its children. This allows components to access (and manipulate) app-level data without needing to pass props around. Svelte 2 provides a convenient `$` syntax for referencing store properties: + +```html +

Hello {$name}!

+``` + +Similarly, store methods can be invoked via event listeners: + +```html + +``` + +The store can also be accessed programmatically: + +```js +const { foo } = this.store.get(); +this.store.doSomething(); +``` + +Since there is no longer a `this`, we need to reconsider this approach. At the same time, the `get`/`set`/`on`/`fire` interface (designed to mirror the component API) feels slightly anachronistic in light of the rest of this RFC. A major problem with the store in its current incarnation is its lack of typechecker-friendliness, and the undocumented hoops necessary to jump through to integrate with popular libraries like MobX. + +I'm not yet sure how the store fits into this proposal — this is perhaps the biggest unknown at this point. + + +### Spread props + +It's convenient to be able to pass down props from a parent to a child component without knowing what they are. In Svelte 2 we can do this — it's hacky but it works: + +```html + +{#await loader() then mod} + +{/await} + + +``` + +That opportunity no longer exists in this RFC. Instead we need some other way to express the concept of 'all the properties that were passed into this component'. The best I can come up with is an injected variable with a name like `__props__`: + +```html + + + + +``` + +This is inelegant, but I believe the inelegance would affect a small minority of components. + +> Since a component's public API ordinarily uses accessors to define the component's contract, it is likely that *inline* components would need to use a privileged non-public API, so that unanticipated props could be passed in. (This is something we should do anyway) + + ### Sync vs async rendering One of the things that differentiates Svelte from other frameworks is that updates are synchronous. In other words: @@ -808,9 +867,12 @@ Potentially, this could be alleviated with compiler magic (i.e. detecting multip ### svelte-extras +TODO + ### Examples +TODO ## How we teach this From 0233a95e9d40fb2dc151dd0cf8723423e16db17e Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 2 Nov 2018 12:09:13 -0400 Subject: [PATCH 20/34] svelte-extras --- text/0000-reactive-assignments.md | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/text/0000-reactive-assignments.md b/text/0000-reactive-assignments.md index 4684952..c1b0432 100644 --- a/text/0000-reactive-assignments.md +++ b/text/0000-reactive-assignments.md @@ -867,7 +867,34 @@ Potentially, this could be alleviated with compiler magic (i.e. detecting multip ### svelte-extras -TODO +Most of the methods in [svelte-extras](https://github.com/sveltejs/svelte-extras) no longer make sense. The two that we do want to reimplement are `tween` and `spring`. + +Happily, these will no longer involve monkey-patching components: + +```html + + + +``` ### Examples From d92efc19f1ef210b80c267e9fe1b0aa609476630 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 2 Nov 2018 12:17:55 -0400 Subject: [PATCH 21/34] examples --- text/0000-reactive-assignments.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/text/0000-reactive-assignments.md b/text/0000-reactive-assignments.md index c1b0432..5d415ee 100644 --- a/text/0000-reactive-assignments.md +++ b/text/0000-reactive-assignments.md @@ -899,7 +899,13 @@ Happily, these will no longer involve monkey-patching components: ### Examples -TODO +* markdown editor [current](https://svelte.technology/repl?version=2.15.0&demo=binding-textarea) / [imagined](https://svelte.technology/repl?version=2.15.0&gist=a0443aa0fc68947b5fad8fae1aa63627) +* media elements [current](https://svelte.technology/repl?version=2.15.0&demo=binding-media-elements) / [imagined](https://svelte.technology/repl?version=2.15.0&gist=8562a70e1a1e708b735b7a50e80b3cfe) +* nested components [current](https://svelte.technology/repl?version=2.15.0&demo=nested-components) / [imagined](https://svelte.technology/repl?version=2.15.0&gist=cbad006c2197633ac75fc8f31c3670df) +* SVG clock [current](https://svelte.technology/repl?version=2.15.0&demo=svg-clock) / [imagined](https://svelte.technology/repl?version=2.15.0&gist=758aacc71f506518a70c0e42f0735f6c) +* Simple transition [current](https://svelte.technology/repl?version=2.15.0&demo=transitions-fade) / [imagined](https://svelte.technology/repl?version=2.15.0&gist=7bd7b4349a27342ca96b58046eb6e765) +* Custom transition [current](https://svelte.technology/repl?version=2.15.0&demo=transitions-custom) / [imagined](https://svelte.technology/repl?version=2.15.0&gist=2e9830aad338305877d226d80a9d04d4) +* Temperature converter [current](https://svelte.technology/repl?version=2.15.0&demo=7guis-temperature) / [imagined](https://svelte.technology/repl?version=2.15.0&gist=439fc5e7c89d91a31c9c9c1447434532) ## How we teach this From 79c95cc7a5d9bb65731cf27ef5b61dec5afa0578 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 2 Nov 2018 12:38:21 -0400 Subject: [PATCH 22/34] bottom sections --- text/0000-reactive-assignments.md | 32 +++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/text/0000-reactive-assignments.md b/text/0000-reactive-assignments.md index 5d415ee..2840689 100644 --- a/text/0000-reactive-assignments.md +++ b/text/0000-reactive-assignments.md @@ -899,6 +899,8 @@ Happily, these will no longer involve monkey-patching components: ### Examples +The 'imagined' components are REPL links, but they do not work in the REPL (obviously) — they're just there so it's easy to see the before/after side-by-side. + * markdown editor [current](https://svelte.technology/repl?version=2.15.0&demo=binding-textarea) / [imagined](https://svelte.technology/repl?version=2.15.0&gist=a0443aa0fc68947b5fad8fae1aa63627) * media elements [current](https://svelte.technology/repl?version=2.15.0&demo=binding-media-elements) / [imagined](https://svelte.technology/repl?version=2.15.0&gist=8562a70e1a1e708b735b7a50e80b3cfe) * nested components [current](https://svelte.technology/repl?version=2.15.0&demo=nested-components) / [imagined](https://svelte.technology/repl?version=2.15.0&gist=cbad006c2197633ac75fc8f31c3670df) @@ -910,32 +912,30 @@ Happily, these will no longer involve monkey-patching components: ## How we teach this -> What names and terminology work best for these concepts and why? How is this -idea best presented? As a continuation of existing Svelte patterns, or as a -wholly new one? +The success of this idea hinges on whether we can explain how **reactive assignments** work to developers who aren't compiler nerds. That means lots of examples that emphasise showing the compiled output, so that developers can understand what is happening to their code even if they don't need to care about it day-to-day. -> Would the acceptance of this proposal mean the Svelte guides must be -re-organized or altered? Does it change how Svelte is taught to new users -at any level? +It is also essential to have plentiful examples showing how existing Svelte patterns can be implemented in this new world. -> How should this feature be introduced and taught to existing Svelte -users? ## Drawbacks -> Why should we *not* do this? Please consider the impact on teaching Svelte, -on the integration of this feature with other existing and planned features, -on the impact of the API churn on existing apps, etc. +Obviously, this is a breaking change. We're not the React team; we don't have the resources to support two separate paradigms. + +It also introduces some frankly somewhat surprising behaviour. Having spent much of the week thinking about it, and toying with existing components, I do earnestly believe that this approach feels natural once you're over the initial shock, but not everyone is guaranteed to feel that way. + +The loss of computed properties as a distinct primitive will be felt by some Svelte users. It's no longer as convenient to use a value that is derived from other derived values, particularly if that value is a function. + +Overall though, this solves so many inter-related problems with Svelte that I believe the benefits to be overwhelming. -> There are tradeoffs to choosing any path, please attempt to identify them here. ## Alternatives -> What other designs have been considered? What is the impact of not doing this? +We toyed with a few alternative concepts: -> This section could also include prior art, that is, how other frameworks in the same domain have solved this problem. +* Not doing anything, and attempting to fix the niggly edge cases within the current paradigm +* Adopting something akin to React Hooks. This was met with a negative reaction from the community. Despite some minor ergonomic advantages in certain cases, the downsides of Hooks (the 'rules', the reliance on repeatedly calling user code, etc) were considered greater than the advantages +* Pursuing a purer vision of reactive programming, akin to [that described by Paul Stovell](http://paulstovell.com/blog/reactive-programming). This would arguably make code more difficult to reason about, not less, and would likely introduce difficult syntactical requirements ## Unresolved questions -> Optional, but suggested for first drafts. What parts of the design are still -TBD? +The major TODOs centre around `Store` and spread properties. From 5d42870f390afa94d4ee1b969d25c6dadf5885d7 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 2 Nov 2018 12:50:46 -0400 Subject: [PATCH 23/34] add note --- text/0000-reactive-assignments.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/text/0000-reactive-assignments.md b/text/0000-reactive-assignments.md index 2840689..02c0ded 100644 --- a/text/0000-reactive-assignments.md +++ b/text/0000-reactive-assignments.md @@ -379,6 +379,11 @@ onupdate(once(() => { ...but we could decide (🐃) to include `oncreate` as a convenience anyway. +--- + +The remainder of this section will address how existing Svelte concepts are affected by this RFC. Some things (actions, transitions etc) are unmentioned because they are unaffected, though if items are missing please raise your voice. + + ### Refs Refs are references to DOM nodes: From ab2b7833517016b196b294521bb7d5b81fb9533b Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 2 Nov 2018 13:03:50 -0400 Subject: [PATCH 24/34] remove bit about pkg.devDependencies --- text/0000-reactive-assignments.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/text/0000-reactive-assignments.md b/text/0000-reactive-assignments.md index 02c0ded..de21f54 100644 --- a/text/0000-reactive-assignments.md +++ b/text/0000-reactive-assignments.md @@ -660,8 +660,6 @@ I don't think the API needs to change. It's not clear how this proposal affects standalone components (as opposed to those that use shared helpers). Perhaps it doesn't? -One consequence of the new way of doing things: Svelte no longer lives in `pkg.devDependencies`, but would instead need to become a `dependency`. Not very On Brand but probably a small price to pay. - ### Store From cbc9bd68467d5717ca08fc9dbb7a7162a453968a Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 2 Nov 2018 15:38:00 -0400 Subject: [PATCH 25/34] refine language around exports --- text/0000-reactive-assignments.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/text/0000-reactive-assignments.md b/text/0000-reactive-assignments.md index de21f54..36b4a3f 100644 --- a/text/0000-reactive-assignments.md +++ b/text/0000-reactive-assignments.md @@ -197,7 +197,7 @@ const Component = defineComponent((__update) => { Many frameworks (and also web components) have a conceptual distinction between 'props' (values passed *to* a component) and 'state' (values that are internal to the component). Svelte does not. This is a shortcoming. -The proposed solution is to use the `export` keyword to declare a value that can be set from outside as a prop: +There is a solution to this problem, but brace yourself — this may feel a little weird at first: ```html From 0811b9d896fb19573091783b79e7a6f67c303451 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Fri, 2 Nov 2018 19:05:29 -0400 Subject: [PATCH 28/34] rename functions to onprops and onmount --- text/0000-reactive-assignments.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/text/0000-reactive-assignments.md b/text/0000-reactive-assignments.md index 3369a44..7c05787 100644 --- a/text/0000-reactive-assignments.md +++ b/text/0000-reactive-assignments.md @@ -295,15 +295,15 @@ export function use_interval(fn, ms) { > This might seem less ergonomic than React Hooks, whereby you can do `const time = useCustomHook()`. The payoff is that you don't need to run that code on every single state change. -There are two other lifecycle functions required — (🐃) `onstate` and (🐃) `onupdate`: +There are two other lifecycle functions required — (🐃) `onprops` (similar to `onstate` in Svelte v2) and (🐃) `onupdate`: ```html ``` -Under this proposal, the `onstate` callback runs whenever props change but *not* when private state changes. This makes cycles impossible: +Under this proposal, the `onprops` callback runs whenever props change but *not* when private state changes. This makes cycles impossible: ```html ``` -That opportunity no longer exists in this RFC. Instead we need some other way to express the concept of 'all the properties that were passed into this component'. The best I can come up with is an injected variable with a name like `__props__`: +That opportunity no longer exists in this RFC. Instead we need some other way to express the concept of 'all the properties that were passed into this component'. One suggestion is to use the `bind` directive on (🐃) the `` element described above: ```html + + - + ``` -This is inelegant, but I believe the inelegance would affect a small minority of components. - > Since a component's public API ordinarily uses accessors to define the component's contract, it is likely that *inline* components would need to use a privileged non-public API, so that unanticipated props could be passed in. (This is something we should do anyway) From 0da3b7eb4c0c3b917b7f13fc7d25c69e2db431a2 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 3 Nov 2018 09:56:17 -0400 Subject: [PATCH 31/34] tweak interval example --- text/0000-reactive-assignments.md | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/text/0000-reactive-assignments.md b/text/0000-reactive-assignments.md index 84621ef..6e1b22a 100644 --- a/text/0000-reactive-assignments.md +++ b/text/0000-reactive-assignments.md @@ -269,19 +269,6 @@ That setup code could include work that needs to be undone when the component is The `ondestroy` callback is associated with the component instance because it is called during instantiation; no compiler magic is involved. Beyond that (if it is called outside instantiation, an error is thrown), there are no rules or restrictions as to when a lifecycle function is called. Reusability is therefore straightforward: -```html - - -

The time is {format_time(time)}

-``` - ```js // helpers.js import { ondestroy } from 'svelte'; @@ -293,7 +280,18 @@ export function use_interval(fn, ms) { } ``` -> This might seem less ergonomic than React Hooks, whereby you can do `const time = useCustomHook()`. The payoff is that you don't need to run that code on every single state change. +```html + + +

The time is {format_time(time)}

+``` + +> This might seem less ergonomic than React Hooks, whereby you can do `const time = useCustomHook()`. The payoff is that you don't need to run that code on every single state change, and it's easier to see which values in a component are subject to change. There are two other lifecycle functions required — (🐃) `onprops` (similar to `onstate` in Svelte v2) and (🐃) `onupdate`: From 38d1466ffaf048a2d02cd255edd0e911ccd3bc91 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 3 Nov 2018 09:57:08 -0400 Subject: [PATCH 32/34] remove WIP warning --- text/0000-reactive-assignments.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/text/0000-reactive-assignments.md b/text/0000-reactive-assignments.md index 6e1b22a..4543a93 100644 --- a/text/0000-reactive-assignments.md +++ b/text/0000-reactive-assignments.md @@ -8,8 +8,6 @@ This RFC proposes a radically new component design that solves the biggest problems with Svelte in its current incarnation, resulting in smaller apps and less code to write. It does so by leveraging Svelte's unique position as a compiler to solve the problem of *reactivity* at the language level. -**This RFC is a work-in-progress, and some pieces are missing!** - ## Motivation Many developers report being intrigued by Svelte, but unwilling to accommodate some of the quirks of its design. For example, using features like nested components or transitions requires that you *register* them, involving unfortunate boilerplate: From 50ae5029aca6f1b142347459606624db4a123fab Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 3 Nov 2018 10:00:10 -0400 Subject: [PATCH 33/34] rename to 0001, add links --- ...0-reactive-assignments.md => 0001-reactive-assignments.md} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename text/{0000-reactive-assignments.md => 0001-reactive-assignments.md} (99%) diff --git a/text/0000-reactive-assignments.md b/text/0001-reactive-assignments.md similarity index 99% rename from text/0000-reactive-assignments.md rename to text/0001-reactive-assignments.md index 4543a93..727c330 100644 --- a/text/0000-reactive-assignments.md +++ b/text/0001-reactive-assignments.md @@ -1,6 +1,6 @@ - Start Date: 2018-11-01 -- RFC PR: (leave this empty) -- Svelte Issue: (leave this empty) +- RFC PR: https://github.com/sveltejs/rfcs/pull/1 +- Svelte Issue: https://github.com/sveltejs/svelte/issues/1826 # Reactive assignments From 543f42d0a8e3d2746093500b9baeeb1f2705ca1d Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Sat, 3 Nov 2018 10:03:20 -0400 Subject: [PATCH 34/34] where are my semicolons? im a monster --- text/0001-reactive-assignments.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/text/0001-reactive-assignments.md b/text/0001-reactive-assignments.md index 727c330..eb3af7c 100644 --- a/text/0001-reactive-assignments.md +++ b/text/0001-reactive-assignments.md @@ -24,7 +24,7 @@ Many developers report being intrigued by Svelte, but unwilling to accommodate s export default { components: { Nested }, transitions: { fade } - } + }; ``` @@ -42,7 +42,7 @@ Declaring methods is also somewhat cumbersome, and in many editor configurations const { foo } = this.get(); } } - } + }; ```