├── .gitignore ├── text ├── 0008-style-directives.md ├── 0005-svelte-component-interface.md ├── 0007-markup-constants.md ├── 0006-style-properties.md ├── 0004-better-composition.md ├── 0003-reactive-declarations.md ├── 0002-reactive-stores.md └── 0001-reactive-assignments.md ├── 0000-template.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /scratch -------------------------------------------------------------------------------- /text/0008-style-directives.md: -------------------------------------------------------------------------------- 1 | - Start Date: 2020-12-09 2 | - RFC PR: [#42](https://github.com/sveltejs/rfcs/pull/42) 3 | - Svelte Issue: (leave this empty) 4 | 5 | # Style Directive 6 | 7 | ## Summary 8 | 9 | Add a `style:` directive: 10 | 11 | ```svelte 12 |
13 | ``` 14 | 15 | ## Motivation 16 | 17 | Currently, when inline style attributes need to be set based on multiple variables, the construction of a style attribute string is left to the developer. 18 | 19 | Constructing these attribute strings can become somewhat clumsy. 20 | 21 | For example: 22 | 23 | ```svelte 24 | 31 | ``` 32 | 33 | It would be useful — and often less error-prone — to be able to set multiple inline styles without having to construct the attribute string: 34 | 35 | ```svelte 36 | 41 | ``` 42 | 43 | ## Detailed design 44 | 45 | Style directives would take the form `style:property="value"` where `property` is a CSS property and `value` is a CSS property value. 46 | 47 | ### Shorthand 48 | 49 | A shorthand form could also exist when a variable name and CSS property name match: 50 | 51 | ```svelte 52 | 55 | 56 | ``` 57 | 58 | ### Conflicting Styles 59 | 60 | One question surrounding this approach how to combine style directives with style attribures that have conflicting properties. 61 | 62 | Consider the example: 63 | 64 | ```svelte 65 | 66 | ``` 67 | 68 | What would be the value of this element's `left` property? 69 | 70 | It seems there are three options available: 71 | 72 | 1. Conflicting CSS properties in style directives are not allowed, and a compiler error is thrown. 73 | 2. The style attribute takes precedence. 74 | 3. The style directive takes precedence. 75 | 76 | The third option seems like the most reasonable, and most closely matches the behavior of the `class:` directive. 77 | 78 | For example, class directives take precedence over class attribute strings: 79 | 80 | ```svelte 81 | 84 | ``` 85 | 86 | ### CSS Variables 87 | 88 | These directives could also contain CSS variables: 89 | 90 | ```svelte 91 | 92 | ``` 93 | 94 | ## How we teach this 95 | 96 | This is pretty similar to the `class:` directive, except that a CSS value is used instead of a boolean. 97 | 98 | ## Drawbacks 99 | 100 | As with any feature, this would add some complexity to the codebase, and add a bit of learning curve, although the impact seems minimal. 101 | 102 | We may want to discourage the construction of complex inline styles based on variables, and encourage the use of CSS variables instead. 103 | Of course, dynamic CSS variables would also need to be passed into a style attribute. 104 | 105 | ## Alternatives 106 | 107 | React uses a style object: 108 | 109 | ```jsx 110 | 111 | ``` 112 | 113 | We could instead mimic React and allow this sort of object to be passed to an element's `style` attribute. 114 | 115 | ## Unresolved questions 116 | 117 | ### Camel Case Property Names? 118 | 119 | We could also allow for the use of camel case property names. 120 | 121 | This would make using variable names that match CSS properties a bit easier: 122 | 123 | ```svelte 124 | 127 | 128 | ``` 129 | 130 | -------------------------------------------------------------------------------- /0000-template.md: -------------------------------------------------------------------------------- 1 | - Start Date: (fill me in with today's date, YYYY-MM-DD) 2 | - RFC PR: (leave this empty) 3 | - Svelte Issue: (leave this empty) 4 | 5 | # (RFC title goes here) 6 | 7 | ## Summary 8 | 9 | > One paragraph explanation of the feature. 10 | 11 | ## Motivation 12 | 13 | > Why are we doing this? What use cases does it support? 14 | 15 | > Please provide specific examples. If you say "this would be more flexible" then 16 | > give an example of something that becomes easier. If you say "this would be make 17 | > it easier to do X" then give an example of what that looks like today and what's 18 | > hard about it. 19 | 20 | > Don't assume that others recognize the problem is one that needs to be solved 21 | > Is there some concrete issue you cannot accomplish without this? 22 | > What does it look like to accomplish some set of goals today and how could 23 | > that be improved? 24 | > Are there any workarounds that are necessary today? 25 | > Are there open issues on Github where people would be helped by this? 26 | > Will the change have performance impacts? Can you quantify them? 27 | 28 | > Please focus on explaining the motivation so that if this RFC is not accepted, 29 | > the motivation could be used to develop alternative solutions. In other words, 30 | > enumerate the constraints you are trying to solve without coupling them too 31 | > closely to the solution you have in mind. 32 | 33 | ## Detailed design 34 | 35 | ### Technical Background 36 | 37 | > There are a lot of ways Svelte is used. It's hosted on different platforms; 38 | > integrated with different libraries; built with different bundlers, etc. No one 39 | > person knows everything about all the ways Svelte is used. What does someone who 40 | > knows about Svelte but hasn't necessarily used anything outside of it need to 41 | > know? Are there docs you can share? 42 | 43 | > How do different libraries or frameworks implement this feature? We can take 44 | > design inspiration from others who have done this well and improve upon the 45 | > designs or make them better fit Svelte. 46 | 47 | ### Implementation 48 | 49 | > Explain the design in enough detail for somebody familiar with the framework to 50 | understand, and for somebody familiar with the implementation to implement. Any 51 | > new terminology should be defined here. 52 | 53 | > Explain not just the final design, but also how you arrived at it. What 54 | > constraints did you face? Are there corner cases you've come up with solutions for? 55 | 56 | > Explain how your design fits into the larger picture. Are the other open problems 57 | > in this area you're familiar with? How does this design fit with potential 58 | > solutions for those issues? 59 | 60 | > Connect your design to the motivations you listed above. When describing a part of 61 | > the design, it can be useful to share an example of what it would look like to 62 | > utilize the implementation as solution to the problem. 63 | 64 | ## How we teach this 65 | 66 | > What names and terminology work best for these concepts and why? How is this 67 | idea best presented? As a continuation of existing Svelte patterns, or as a 68 | wholly new one? 69 | 70 | > Would the acceptance of this proposal mean the Svelte guides must be 71 | re-organized or altered? Does it change how Svelte is taught to new users 72 | at any level? 73 | 74 | > How should this feature be introduced and taught to existing Svelte 75 | users? 76 | 77 | ## Drawbacks 78 | 79 | > Why should we *not* do this? Please consider the impact on teaching Svelte, 80 | on the integration of this feature with other existing and planned features, 81 | on the impact of the API churn on existing apps, etc. 82 | 83 | > There are tradeoffs to choosing any path, please attempt to identify them here. 84 | 85 | ## Alternatives 86 | 87 | > What other designs have been considered? What is the impact of not doing this? 88 | 89 | > This section could also include prior art, that is, how other frameworks in the 90 | > same domain have solved this problem differently. 91 | 92 | ## Unresolved questions 93 | 94 | > Optional, but suggested for first drafts. What parts of the design are still TBD? 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Svelte RFCs 2 | 3 | This is a place to discuss major changes to Svelte — where 'major' implies significant changes either to public interfaces or internal implementation details, particularly those that could be controversial or involve breaking changes. 4 | 5 | Most changes don't need to go through the Request for Comments (RFC) process and can rely on issues and pull requests. 6 | 7 | A huge part of the value on an RFC is defining the problem clearly, collecting use cases, showing how others have solved a problem, etc. Coming up with a design is very iterative and only one part of the process. An RFC can provide tremendous value without the design described in it being accepted. 8 | 9 | The is inspired by (which is to say shamelessly ripped off from) the RFC process adopted by [React](https://github.com/reactjs/rfcs), [Ember](https://github.com/emberjs/rfcs) and others. The process itself is subject to change (or even abandonment!) as we gain experience with it. 10 | 11 | 12 | ## The process 13 | 14 | * Fork the RFC repo http://github.com/sveltejs/rfcs 15 | * Copy 0000-template.md to text/0000-my-feature.md (where 'my-feature' is descriptive. Don't assign an RFC number yet). 16 | Fill in the RFC. Put care into the details: **RFCs that do not present convincing motivation, demonstrate understanding of the impact of the design, or are disingenuous about the drawbacks or alternatives tend to be poorly-received.** 17 | * Submit a pull request. As a pull request the RFC will receive design feedback from the larger community, and the author should be prepared to revise it in response. 18 | * Build consensus and integrate feedback. RFCs that have broad support are much more likely to make progress than those that don't receive any comments. 19 | * Eventually, the team will decide whether the RFC is a candidate for inclusion in Svelte. 20 | RFCs that are candidates for inclusion in Svelte will enter a "final comment period" lasting 3 calendar days. The beginning of this period will be signaled with a comment and tag on the RFCs pull request. 21 | * An RFC can be modified based upon feedback from the team and community. Significant modifications may trigger a new final comment period. 22 | An RFC may be rejected by the team after public discussion has settled and comments have been made summarizing the rationale for rejection. A member of the team should then close the RFCs associated pull request. 23 | * An RFC may be accepted at the close of its final comment period. A team member will merge the RFCs associated pull request, at which point the RFC will become 'active'. 24 | 25 | 26 | ## The RFC life-cycle 27 | 28 | Once an RFC becomes active, then authors may implement it and submit the feature as a pull request to the Svelte repo. Becoming 'active' is not a rubber stamp, and in particular still does not mean the feature will ultimately be merged; it does mean that the core team has agreed to it in principle and are amenable to merging it. 29 | 30 | Furthermore, the fact that a given RFC has been accepted and is 'active' implies nothing about what priority is assigned to its implementation, nor whether anybody is currently working on it. 31 | 32 | Modifications to active RFCs can be done in followup PRs. We strive to write each RFC in a manner that it will reflect the final design of the feature; but the nature of the process means that we cannot expect every merged RFC to actually reflect what the end result will be at the time of the next major release; therefore we try to keep each RFC document somewhat in sync with the language feature as planned, tracking such changes via followup pull requests to the document. 33 | 34 | 35 | ## Implementing an RFC 36 | 37 | The author of an RFC is not obligated to implement it. Of course, the RFC author (like any other developer) is welcome to post an implementation for review after the RFC has been accepted. 38 | 39 | If you are interested in working on the implementation for an 'active' RFC, but cannot determine if someone else is already working on it, feel free to ask (e.g. by leaving a comment on the associated issue). 40 | 41 | 42 | ## Reviewing RFCs 43 | 44 | We tend to do our thinking informally, in the open, when time allows. There are a large number of community members relative to a small number of a core contributors who have many responsibilities. You can help ensure your RFC is reviewed in a timely manner by putting in the time to think through the various details discussed in the template. It doesn't scale to push the thinking onto a small number of core contributors. If reviewers raise an issue, don't dismiss it as irrelevant, but take the time to provide examples or data explaining it and coming up with ways that the design might be changed in response. Sometimes answering a single question can be very time consuming (such as setting up a benchmark), but discussions tend to stall out if concerns don't get thoroughly addressed. 45 | -------------------------------------------------------------------------------- /text/0005-svelte-component-interface.md: -------------------------------------------------------------------------------- 1 | - Start Date: 2020-09-18 2 | - RFC PR: 3 | - Svelte Issue: 4 | 5 | # Strongly Typed Svelte Component Interface 6 | 7 | ## Summary 8 | 9 | Provide a Svelte component type definition which library authors can use to provide a strongly typed Svelte component which the language server understands. Inside a `.d.ts` file: 10 | 11 | ```ts 12 | import { SvelteComponent } from "svelte"; 13 | 14 | export class MyComponent extends SvelteComponent<{...prop definition}, {...events definition}, {...slots definition}> {} 15 | ``` 16 | 17 | ## Motivation 18 | 19 | Currently there is no official and easy way to provide type definitions. Devs who want to write type definitions for libraries which don't have those, or library authors who want to provide them, have no clear path on how to write these. It's also confusing to them that the current `SvelteComponent` definition does not work for this. 20 | 21 | ## Detailed design 22 | 23 | ### Usage would look like this: 24 | 25 | Inside a `d.ts` file: 26 | 27 | ```ts 28 | import { SvelteComponent } from 'svelte'; 29 | 30 | declare module "myModule" { 31 | export class MyComponent extends SvelteComponent< 32 | { propA: boolean }, 33 | { someEvent: CustomEventthis is a reference error: {area}
131 | ``` 132 | 133 | ### Hoisting and TDZ 134 | 135 | With JavaScript `const` (and `let`), values cannot be read before they have been initialised. We could choose to do the same thing here, though in our case it is an unnecessary restriction — we could very simply hoist values to the top of their block, in the order in which they're declared: 136 | 137 | ```svelte 138 | {#if n} 139 |{n}^4 = {hypercubed}
140 | 141 | {@const squared = n * n} 142 | {@const cubed = squared * n} 143 | {@const hypercubed = cubed * n} 144 | {/if} 145 | ``` 146 | 147 | > 🐃 You could argue that this improves or degrades readability depending on your perspective. You could also argue in favour of topological ordering, as we have in the case of reactive statements/declarations. I don't yet have strong opinions one way or the other. 148 | 149 | ### Conflicts 150 | 151 | Defining the same constant twice in a single block would be a compile error: 152 | 153 | ```svelte 154 | {@const foo = a} 155 | {@const foo = b} 156 | ``` 157 | 158 | 159 | ## How we teach this 160 | 161 | I've provisionally suggested `{@const ...}` for the reasons articulated above, namely the similarities to `const`, so this new feature could be called a 'const tag'. The keyword is open to bikeshedding though — perhaps someone could make a case for calling it `{@value ...}` or `{@local ...}` instead. 162 | 163 | Accepting this proposal wouldn't require any changes to the existing docs, but would require some new documentation to be created. 164 | 165 | ## Drawbacks 166 | 167 | The evergreen answer to the 'why should be *not* do this?' question is 'it increases the amount of stuff to learn, and is another thing that we have to implement and maintain'. 168 | 169 | In this case though there's an additional reason we might consider not adding this. One of the arguments that's frequently deployed in favour of JSX-based frameworks over template-based ones is that JSX allows you to use existing language constructs: 170 | 171 | ```js 172 | {boxes.map(box => { 173 | const area = box.width * box.height; 174 | 175 | returnCopyright {year} SvelteJS Inc
309 |Current theme is {theme}
363 |Twice {count} is {doubled}; twice that is {quadrupled}
22 | 23 | 24 | ``` 25 | 26 | 27 | ## Motivation 28 | 29 | In [RFC 1](https://github.com/sveltejs/rfcs/blob/reactive-assignments/text/0001-reactive-assignments.md), we introduced the idea of *reactive assignments*, in which assigning to a component's local variables... 30 | 31 | ```js 32 | count += 1; 33 | ``` 34 | 35 | ...causes the component to update. 36 | 37 | As an indirect consequence of that change, we're able to get rid of all the boilerplate associated with Svelte 2 components, greatly simplifying both the compiler and the user's application code. But it also means getting rid of [computed properties](https://svelte.technology/guide#computed-properties), which are a powerful and convenient mechanism for push-based reactivity: 38 | 39 | ```html 40 |Twice {count} is {doubled}; twice that is {quadrupled}
41 | 42 | 43 | 44 | 56 | ``` 57 | 58 | The useful thing about computed properties is that they are only recalculated when their inputs change, side-stepping a common performance problem that affects some frameworks in which derived values must be recalculated on every render. Unlike some other frameworks that implement computed properties, the compiler is able to build a dependency graph of computed properties at *compile time*, enabling it to sort them topologically and generate highly efficient code that doesn't depend on expensive *run time* dependency tracking. 59 | 60 | RFC 1 initially glossed over the loss of computed properties, suggesting that we could simply replace them with functions: 61 | 62 | ```html 63 | 68 | 69 |Twice {count} is {doubled()}; twice that is {quadrupled()}
70 | 71 | 72 | ``` 73 | 74 | But in this scenario, `doubled` must be called twice (once directly, once via `quadrupled`). Not only that, but we have to trace which values are read when `doubled` and `quadrupled` are called so that we know when we need to update them; in some cases it would be necessary to bail out of that optimisation and call those functions whenever *anything* changed. In more realistic examples, this results in a lot of extra work relative to Svelte 2. 75 | 76 | The only way to avoid that work is with a [Sufficiently Smart Compiler](http://wiki.c2.com/?SufficientlySmartCompiler). We need an alternative. 77 | 78 | 79 | ## Detailed design 80 | 81 | In [What is Reactive Programming?](http://paulstovell.com/blog/reactive-programming), Paul Stovell introduces the 'destiny operator': 82 | 83 | ``` 84 | var a = 10; 85 | var b <= a + 1; 86 | a = 20; 87 | Assert.AreEqual(21, b); 88 | ``` 89 | 90 | If we could use the destiny operator in components, the Svelte 3 compiler would be able to do exactly what it does with Svelte 2 computed properties — update `b` whenever `a` changes. 91 | 92 | Unfortunately we can't, because that would be invalid JavaScript, and it's important for many reasons that everything inside a component's ` 210 | 211 |{todo.description}
215 | {/each} 216 | ``` 217 | 218 | One limitation of reactive stores is that it's difficult to mix them with a component's local state. For example, if we wanted a filtered view of those todos, we can't simply derive a new store that uses a local filter variable — 219 | 220 | ```html 221 | 228 | 229 |{todo.description}
238 | {/each} 239 | ``` 240 | 241 | — because `filtered` doesn't have a way to know when `hideDone` changes. Instead, we'd need to create a new store: 242 | 243 | ```diff 244 | 254 | 255 |{todo.description}
265 | {/each} 266 | ``` 267 | 268 | Reactive declarations offer an alternative, if we allow the same treatment of values with the `$` prefix: 269 | 270 | ```diff 271 | 280 | 281 |{todo.description}
291 | {/each} 292 | ``` 293 | 294 | The obvious problem with this is that `$todos` isn't defined anywhere in the ` 79 | 80 |{todo.description}
302 | {/each} 303 | ``` 304 | 305 | 306 | ### Relationship with TC39 Observables 307 | 308 | There is a [stage 1 proposal for an Observable object](https://github.com/tc39/proposal-observable) in JavaScript itself. 309 | 310 | 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: 311 | 312 | * They don't represent a single value changing over time, but rather a stream of distinct values. This is a subtle but important distinction 313 | * 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 314 | * Observables can 'complete', but declarative components (in Svelte and other frameworks) deliberately do not have a concept of time. The two things are incompatible 315 | * 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 316 | 317 | Of course, some Observables *are* suitable for representing reactive values in a template, and they could easily be adapted to work with this design: 318 | 319 | ```js 320 | function adaptor(observable) { 321 | return { 322 | subscribe(fn) { 323 | const subscriber = observable.subscribe({ 324 | next: fn 325 | }); 326 | 327 | return subscriber.unsubscribe; 328 | } 329 | } 330 | } 331 | 332 | const observable = Observable.of('red', 'green', 'blue'); 333 | const store = adaptor(observable); 334 | 335 | const unsubscribe = store.subscribe(color => { 336 | console.log(color); // logs red, then green, then blue 337 | }); 338 | ``` 339 | 340 | 341 | ### Examples of use with existing state management libraries 342 | 343 | 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.) 344 | 345 | 346 | #### Redux 347 | 348 | ```js 349 | // src/redux.js 350 | import { createStore } from 'redux'; 351 | 352 | export const reduxStore = createStore((state = 0, action) => { 353 | switch (action.type) { 354 | case 'INCREMENT': 355 | return state + 1 356 | case 'DECREMENT': 357 | return state - 1 358 | default: 359 | return state 360 | } 361 | }); 362 | 363 | function adaptor(reduxStore) { 364 | return { 365 | subscribe(fn) { 366 | return reduxStore.subscribe(() => { 367 | fn(reduxStore.getState()); 368 | }); 369 | } 370 | }; 371 | } 372 | 373 | export const store = adaptor(reduxStore); 374 | ``` 375 | 376 | ```html 377 | 378 | 381 | 382 | 385 | ``` 386 | 387 | 388 | #### Immer 389 | 390 | ```js 391 | import { writable } from 'svelte/store.js'; 392 | import { produce } from 'immer'; 393 | 394 | function immerObservable(data) { 395 | const store = writable(data); 396 | 397 | function update(fn) { 398 | store.update(state => produce(state, fn)); 399 | } 400 | 401 | return { 402 | update, 403 | subscribe: store.subscribe 404 | }; 405 | } 406 | 407 | const todos = immerObservable([ 408 | { done: false, description: 'walk the dog' }, 409 | { done: false, description: 'mow the lawn' }, 410 | { done: false, description: 'do the laundry' } 411 | ]); 412 | 413 | todos.update(draft => { 414 | draft[0].done = true; 415 | }); 416 | ``` 417 | 418 | 419 | #### Shiz 420 | 421 | ```js 422 | import { readable } from 'svelte/store.js'; 423 | import { value, computed } from 'shiz'; 424 | 425 | const a = value(1); 426 | const b = computed([a], ([a]) => a * 2); 427 | 428 | function shizObservable(shiz) { 429 | return readable(function start(set) { 430 | return shiz.on('change', () => { 431 | set(shiz.get()); 432 | }); 433 | }, shiz.get()); 434 | } 435 | 436 | const store = shizObservable(b); 437 | 438 | const unsubscribe = store.subscribe(value => { 439 | console.log(value); // logs 2 440 | }); 441 | 442 | a.set(2); // logs 4 443 | ``` 444 | 445 | 446 | ### Using with Sapper 447 | 448 | 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. 449 | 450 | 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.) 451 | 452 | > Potential corner-cases to discuss: 453 | > * What happens if a subscriber causes another subscriber to be removed (e.g. it results in a component subtree being destroyed)? 454 | > * Is `$user.name` ambiguous (i.e. is `user` the store, or `user.name`?) and if so how do we resolve the ambiguity 455 | > * What happens if `$user` is declared in a scope that has a store `user`? Do we just not subscribe? 456 | 457 | 458 | ## How we teach this 459 | 460 | 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. 461 | 462 | It's arguably simpler to teach than the existing store, since it's purely concerned with data, and avoids the 'magic' of auto-attaching. 463 | 464 | 465 | ## Drawbacks 466 | 467 | 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. 468 | 469 | 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. 470 | 471 | > [RFC 3](https://github.com/sveltejs/rfcs/blob/reactive-declarations/text/0003-reactive-declarations.md) presents an escape hatch to this problem 472 | 473 | 474 | ## Alternatives 475 | 476 | * The existing store (but only at the top level; there is no opportunity to add stores at a lower level in the new design) 477 | * Baking in an existing state management library (and its opinions) 478 | * Using TC39 Observables 479 | * Not having any kind of first-class treatment of reactive values, and relying on the reactive assignments mechanism exclusively 480 | 481 | 482 | ## Unresolved questions 483 | 484 | * The Sapper question 485 | * The exact mechanics of how typechecking would work 486 | -------------------------------------------------------------------------------- /text/0001-reactive-assignments.md: -------------------------------------------------------------------------------- 1 | - Start Date: 2018-11-01 2 | - RFC PR: https://github.com/sveltejs/rfcs/pull/4 3 | - Svelte Issue: https://github.com/sveltejs/svelte/issues/1826 4 | 5 | # Reactive assignments 6 | 7 | ## Summary 8 | 9 | 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. 10 | 11 | ## Motivation 12 | 13 | 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: 14 | 15 | ```html 16 |Your name has {len()} letters in it.
95 | ``` 96 | 97 | Why can't we? Because of *reactivity*. There's no way for the view to 'know' if `name` has changed. 98 | 99 | 100 | ## The reactivity problem 101 | 102 | 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. 103 | 104 | Reactivity is implemented in a variety of different ways (note: I am not expert in these frameworks other than Svelte, please correct any misapprehensions): 105 | 106 | * **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. 107 | 108 | * **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). 109 | 110 | * **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. 111 | 112 | 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*? 113 | 114 | This proposal outlines a novel (as far as we're aware) solution, while bringing a number of significant benefits: 115 | 116 | * Easier-to-grok design 117 | * Less application code 118 | * Smaller bundles 119 | * Simpler, more robust lifecycles 120 | * A realistic path towards first-class TypeScript support 121 | * Well-defined contracts between components and their consumers 122 | * Opportunities to adopt ideas like [time slicing and Suspense](https://auth0.com/blog/time-slice-suspense-react16/) 123 | 124 | 125 | ## Detailed design 126 | 127 | Consider a counter component, the workhorse of examples such as these: 128 | 129 | ```html 130 | 133 | 134 | 137 | ``` 138 | 139 | > The quotes around the event handler are unnecessary as far as Svelte is concerned, but included for the sake of non-Svelte-aware syntax highlighting 140 | 141 | Here, we have declared a single state variable, `count`, with an initial value of `0`. The code inside the ` 163 | 164 | 167 | ``` 168 | 169 | ...would be transformed to something like this: 170 | 171 | ```js 172 | function create_main_fragment(component, ctx) { 173 | // ... the code that creates and maintains the view — 174 | // this will be relatively unaffected by this proposal 175 | } 176 | 177 | const Component = defineComponent((__update) => { 178 | let count = 0; 179 | const incr = () => { 180 | count += 1; 181 | __update({ count: true }); 182 | }; 183 | 184 | return [ 185 | // this creates the top-level `ctx` variable for `create_main_fragment` 186 | () => ({ count }) 187 | ]; 188 | }, create_main_fragment); 189 | ``` 190 | 191 | **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. 192 | 193 | 194 | ### Mutating objects and arrays 195 | 196 | While it's good practice to use immutable data in components, Svelte should not force developers to do so. Assigning to a property of an object or array would have the same basic behaviour — `a.b = c` would trigger an update on `a`. It's possible that changes could be tracked at a more granular level (i.e. tracking `a.b` rather than `a`, so that `{a.d}
` is unaffected), but these are implementation details beyond the scope of this RFC. 197 | 198 | Calling array methods would *not* trigger updates on those arrays. 199 | 200 | > `x = x` can be used to 'force' an update on `x`, in the rare cases it is necessary 201 | 202 | 203 | ### Props 204 | 205 | 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. 206 | 207 | There is a solution to this problem, but brace yourself — this may feel a little weird at first: 208 | 209 | ```html 210 | 213 | 214 |The time is {format_time(time)}
274 | ``` 275 | 276 | 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: 277 | 278 | ```js 279 | // helpers.js 280 | import { onDestroy } from 'svelte'; 281 | 282 | export function useInterval(fn, ms) { 283 | const interval = setInterval(fn, ms); 284 | onDestroy(() => clearInterval(interval)); 285 | fn(); 286 | } 287 | ``` 288 | 289 | ```html 290 | 296 | 297 |The time is {formatTime(time)}
298 | ``` 299 | 300 | > 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, and *when* a specific value is changing. 301 | 302 | There are three other lifecycle functions required — `onMount` (similar to `oncreate` in Svelte v2), `beforeUpdate` (similar to `onstate`) and `afterUpdate` (similar to `onupdate`): 303 | 304 | ```html 305 | 329 | ``` 330 | 331 | > Note that the `changed`, `current` and `previous` arguments, present in Svelte 2, are absent from these callbacks — I believe they are now unnecessary. 332 | 333 | 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: 334 | 335 | ```js 336 | 356 | ``` 357 | 358 | Under this proposal, `beforeUpdate` callbacks run only once per cycle. This makes infinite loops impossible: 359 | 360 | ```html 361 | 378 | ``` 379 | 380 | > Note that `previous_temperature`, if unused in the view, will not get the reactive treatment. 381 | 382 | Any `afterUpdate` callbacks would run after the view was updated, whether as a result of prop or state changes. Assignments in an `afterUpdate` callback would result in an immediate re-render (once all `afterUpdate` callbacks have run, instead of on the next frame) but would *not* cause the callback to run again. This would allow components to respond to layout changes, for example. 383 | 384 | 385 | --- 386 | 387 | The remainder of this section will address how existing Svelte concepts are affected by this RFC. Some things (CSS etc) are unmentioned because they are unaffected, though if items are missing please raise your voice. 388 | 389 | 390 | ### Directives 391 | 392 | In Svelte 2 there are six different kinds of directive: 393 | 394 | * `on:[event]` and `on:[event]=[callExpression]` 395 | * `use:[action]` and `use:[action]=[argument]` 396 | * `ref:[name]` 397 | * `in:[transition]`, `out:[transition]` and `transition:[transition]`, and `in:[transition]=[params]` etc 398 | * `bind:[name]` and `bind:remote=[local]` 399 | * `class:[name]` and `class:[name]=[expression]` 400 | 401 | Currently, directive values do not have curly braces as delimiters. This is an occasional source of confusion (and compiler complexity) that can be resolved by making directives more like attributes: 402 | 403 | ```html 404 | 405 |now you see me
656 | {/if} 657 |{thing}
705 | {/each} 706 | ``` 707 | 708 | `things` would be injected into the instance ` 782 | ``` 783 | 784 | 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 `the list is {listHeight}px tall:
837 | 838 |{bar}
886 | {/if} 887 | 888 | 900 | ``` 901 | 902 | In the example above, there is no real need to calculate `bar` since it is not rendered, but it happens anyway. 903 | 904 | [RFC 3](https://github.com/sveltejs/rfcs/pull/8) proposed a Svelte 3 friendly alternative to computed properties that is more flexible and involves less boilerplate. 905 | 906 | ### svelte-extras 907 | 908 | 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`. 909 | 910 | Happily, these will no longer involve monkey-patching components: 911 | 912 | ```html 913 | 934 | 935 |