diff --git a/active-rfcs/0013-composition-api.md b/active-rfcs/0013-composition-api.md new file mode 100644 index 00000000..163fdf8e --- /dev/null +++ b/active-rfcs/0013-composition-api.md @@ -0,0 +1,875 @@ +- Start Date: 2019-07-10 +- Target Major Version: 2.x / 3.x +- Reference Issues: [#42](https://github.com/vuejs/rfcs/pull/42) +- Implementation PR: N/A + +> Since this RFC is long, it is deployed in a more readable format [here](https://vue-composition-api-rfc.netlify.com/). There is also an accompanying [API Reference](https://vue-composition-api-rfc.netlify.com/api.html). + +## Summary + +Introducing the **Composition API**: a set of additive, function-based APIs that allow flexible composition of component logic. + +## Basic example + +```html + + + +``` + +## Motivation + +### Logic Reuse & Code Organization + +We all love how Vue is very easy to pick up and makes building small to medium scale applications a breeze. But today as Vue's adoption grows, many users are also using Vue to build large scale projects - ones that are iterated on and maintained over a long timeframe, by a team of multiple developers. Over the years we have witnessed some of these projects run into the limits of the programming model entailed by Vue's current API. The problems can be summarized into two categories: + +1. The code of complex components become harder to reason about as features grow over time. This happens particularly when developers are reading code they did not write themselves. The root cause is that Vue's existing API forces code organization by options, but in some cases it makes more sense to organize code by logical concerns. + +2. Lack of a clean and cost-free mechanism for extracting and reusing logic between multiple components. (More details in [Logic Extraction and Reuse](#logic-extraction-and-reuse)) + +The APIs proposed in this RFC provide the users with more flexibility when organizing component code. Instead of being forced to always organize code by options, code can now be organized as functions each dealing with a specific feature. The APIs also make it more straightforward to extract and reuse logic between components, or even outside components. We will show how these goals are achieved in the [Detailed Design](#detailed-design) section. + +### Better Type Inference + +Another common feature request from developers working on large projects is better TypeScript support. Vue's current API has posed some challenges when it comes to integration with TypeScript, mostly due to the fact that Vue relies on a single `this` context for exposing properties, and that the use of `this` in a Vue component is a bit more magical than plain JavaScript (e.g. `this` inside functions nested under `methods` points to the component instance rather than the `methods` object). In other words, Vue's existing API simply wasn't designed with type inference in mind, and that creates a lot of complexity when trying to make it work nicely with TypeScript. + +Most users who use Vue with TypeScript today are using `vue-class-component`, a library that allows components to be authored as TypeScript classes (with the help of decorators). While designing 3.0, we have attempted to provide a built-in Class API to better tackle the typing issues in a [previous (dropped) RFC](https://github.com/vuejs/rfcs/pull/17). However, as we discussed and iterated on the design, we noticed that in order for the Class API to resolve the typing issues, it must rely on decorators - which is a very unstable stage 2 proposal with a lot of uncertainty regarding its implementation details. This makes it a rather risky foundation to build upon. (More details on Class API type issues [here](#type-issues-with-class-api)) + +In comparison, the APIs proposed in this RFC utilize mostly plain variables and functions, which are naturally type friendly. Code written with the proposed APIs can enjoy full type inference with little need for manual type hints. This also means that code written with the proposed APIs will look almost identical in TypeScript and plain JavaScript, so even non-TypeScript users will potentially be able to benefit from the typings for better IDE support. + +## Detailed Design + +### API Introduction + +Instead of bringing in new concepts, the APIs being proposed here are more about exposing Vue's core capabilities - such as creating and observing reactive state - as standalone functions. Here we will introduce a number of the most fundamental APIs and how they can be used in place of 2.x options to express in-component logic. Note this section focuses on introducing the basic ideas so it does not goes into full details for each API. Full API specs can be found in the [API Reference](https://vue-composition-api-rfc.netlify.com/api.html) section. + +#### Reactive State and Side Effects + +Let's start with a simple task: declaring some reactive state. + +``` js +import { reactive } from 'vue' + +// reactive state +const state = reactive({ + count: 0 +}) +``` + +`reactive` is the equivalent of the current `Vue.observable()` API in 2.x, renamed to avoid confusion with RxJS observables. Here, the returned `state` is a reactive object that all Vue users should be familiar with. + +The essential use case for reactive state in Vue is that we can use it during render. Thanks to dependency tracking, the view automatically updates when reactive state changes. Rendering something in the DOM is considered a "side effect": our program is modifying state external to the program itself (the DOM). To apply and *automatically re-apply* a side effect based on reactive state, we can use the `watch` API: + +``` js +import { reactive, watch } from 'vue' + +const state = reactive({ + count: 0 +}) + +watch(() => { + document.body.innerHTML = `count is ${state.count}` +}) +``` + +`watch` expects a function that applies the desired side effect (in this case, setting `innerHTML`). It executes the function immediately, and tracks all the reactive state properties it used during the execution as dependencies. Here, `state.count` would be tracked as a dependency for this watcher after the initial execution. When `state.count` is mutated at a future time, the inner function will be executed again. + +This is the very essence of Vue's reactivity system. When you return an object from `data()` in a component, it is internally made reactive by `reactive()`. The template is compiled into a render function (think of it as a more efficient `innerHTML`) that makes use of these reactive properties. + +Continuing the above example, this is how we would handle user input: + +``` js +function increment() { + state.count++ +} + +document.body.addEventListener('click', increment) +``` + +But with Vue's templating system we don't need to wrangle with `innerHTML` or manually attaching event listeners. Let's simplify the example with a hypothetical `renderTemplate` method so we can focus on the reactivity side: + +``` js +import { reactive, watch } from 'vue' + +const state = reactive({ + count: 0 +}) + +function increment() { + state.count++ +} + +const renderContext = { + state, + increment +} + +watch(() => { + // hypothetical internal code, NOT actual API + renderTemplate( + ``, + renderContext + ) +}) +``` + +#### Computed State and Refs + +Sometimes we need state that depends on other state - in Vue this is handled with *computed properties*. To directly create a computed value, we can use the `computed` API: + +``` js +import { reactive, computed } from 'vue' + +const state = reactive({ + count: 0 +}) + +const double = computed(() => state.count * 2) +``` + +What is `computed` returning here? If we take a guess at how `computed` is implemented internally, we might come up with something like this: + +``` js +// simplified pseudo code +function computed(getter) { + let value + watch(() => { + value = getter() + }) + return value +} +``` + +But we know this won't work: if `value` is a primitive type like `number`, its connection to the update logic inside `computed` will be lost once it's returned. This is because JavaScript primitive types are passed by value, not by reference: + +![pass by value vs pass by reference](https://www.mathwarehouse.com/programming/images/pass-by-reference-vs-pass-by-value-animation.gif) + +The same problem would occur when a value is assigned to an object as a property. A reactive value wouldn't be very useful if it cannot retain its reactivity when assigned as a property or returned from functions. In order to make sure we can always read the latest value of a computation, we need to wrap the actual value in an object and return that object instead: + +``` js +// simplified pseudo code +function computed(getter) { + const ref = { + value: null + } + watch(() => { + ref.value = getter() + }) + return ref +} +``` + +In addition, we also need to intercept read / write operations to the object's `.value` property to perform dependency tracking and change notification (code omitted here for simplicity). Now we can pass the computed value around by reference, without worrying about losing reactivity. The trade-off is that in order to retrieve the latest value, we now need to access it via `.value`: + +``` js +const double = computed(() => state.count * 2) + +watch(() => { + console.log(double.value) +}) // -> 0 + +state.count++ // -> 2 +``` + +**Here `double` is an object that we call a "ref", as it serves as a reactive reference to the internal value it is holding.** + +> You might be aware that Vue already has the concept of "refs", but only for referencing DOM elements or component instances in templates ("template refs"). Check out [this](https://vue-composition-api-rfc.netlify.com/api.html#template-refs) to see how the new refs system can be used for both logical state and template refs. + +In addition to computed refs, we can also directly create plain mutable refs using the `ref` API: + +``` js +const count = ref(0) +console.log(count.value) // 0 + +count.value++ +console.log(count.value) // 1 +``` + +#### Ref Unwrapping + +::: v-pre +We can expose a ref as a property on the render context. Internally, Vue will perform special treatment for refs so that when a ref is encountered on the render context, the context directly exposes its inner value. This means in the template, we can directly write `{{ count }}` instead of `{{ count.value }}`. +::: + +Here's a version of the same counter example, using `ref` instead of `reactive`: + +``` js +import { ref, watch } from 'vue' + +const count = ref(0) + +function increment() { + count.value++ +} + +const renderContext = { + count, + increment +} + +watch(() => { + renderTemplate( + ``, + renderContext + ) +}) +``` + +In addition, when a ref is nested as a property under a reactive object, it is also automatically unwrapped on access: + +``` js +const state = reactive({ + count: 0, + double: computed(() => state.count * 2) +}) + +// no need to use `state.double.value` +console.log(state.double) +``` + +#### Usage in Components + +Our code so far already provides a working UI that can update based on user input - but the code runs only once and is not reusable. If we want to reuse the logic, a reasonable next step seems to be refactoring it into a function: + +``` js +import { reactive, computed, watch } from 'vue' + +function setup() { + const state = reactive({ + count: 0, + double: computed(() => state.count * 2) + }) + + function increment() { + state.count++ + } + + return { + state, + increment + } +} + +const renderContext = setup() + +watch(() => { + renderTemplate( + ``, + renderContext + ) +}) +``` + +> Note how the above code doesn't rely on the presence of a component instance. Indeed, the APIs introduced so far can all be used outside the context of components, allowing us to leverage Vue's reactivity system in a wider range of scenarios. + + +Now if we leave the tasks of calling `setup()`, creating the watcher, and rendering the template to the framework, we can define a component with just the `setup()` function and the template: + +``` html + + + +``` + +This is the single-file component format we are familiar with, with only the logical part (` +``` + +#### Svelte + +``` html + +``` + +Svelte code looks more concise because it does the following at compile time: + +- Implicitly wraps the entire `