From 903f429696524d8f93b4976d5b09dfb3632e89ef Mon Sep 17 00:00:00 2001 From: Evan You Date: Sat, 8 Jun 2019 00:00:38 +0800 Subject: [PATCH 01/32] function-based api --- active-rfcs/0000-function-api.md | 633 +++++++++++++++++++++++++++++++ 1 file changed, 633 insertions(+) create mode 100644 active-rfcs/0000-function-api.md diff --git a/active-rfcs/0000-function-api.md b/active-rfcs/0000-function-api.md new file mode 100644 index 00000000..bf825cc2 --- /dev/null +++ b/active-rfcs/0000-function-api.md @@ -0,0 +1,633 @@ +- Start Date: 2019-05-30 +- Target Major Version: 2.x / 3.x +- Reference Issues: +- Implementation PR: (leave this empty) + +# Summary + +Expose logic-related component options via function-based APIs instead. + +# Basic example + +``` js +import { value, computed, watch, onMounted } from 'vue' + +const App = { + template: ` +
+ count is {{ count }} + plusOne is {{ plusOne }} + +
+ `, + setup() { + // reactive state + const count = value(0) + // computed state + const plusOne = computed(() => count.value + 1) + // method + const increment = () => { count.value++ } + // watch + watch(() => count.value * 2, val => { + console.log(`count * 2 is ${val}`) + }) + // lifecycle + onMounted(() => { + console.log(`mounted`) + }) + // expose bindings on render context + return { + count, + plusOne, + increment + } + } +} +``` + +# Motivation + +## Logic Composition + +One of the key aspects of the component API is how to encapsulate and reuse logic across multiple components. With Vue 2.x's current API, there are a number of common patterns we've seen in the past, each with its own drawbacks. These include: + +- Mixins (via the `mixins` option) +- Higher-order components (HOCs) +- Renderless components (via scoped slots) + +These patterns are discussed in more details in the [appendix](#prior-art-composition-patterns) - but in general, they all suffer from one or more of the drawbacks below: + +- Unclear sources for properties exposed on the render context. For example, when reading the template of a component using multiple mixins, it can be difficult to tell from which mixin a specific property was injected from. + +- Namespace clashing. Mixins can potentially clash on property and method names, while HOCs can clash on expected prop names. + +- Performance. HOCs and renderless components require extra stateful component instances that come at a performance cost. + +The function based API, inspired by [React Hooks](https://reactjs.org/docs/hooks-intro.html), presents a clean and flexible way to compose logic inside and between components without any of these drawbacks. This can be achieved by extracting code related to a piece of logic into what we call a "composition function" and returning reactive state. Here is an example of using a composition function to extract the logic of listening to the mouse position: + +``` js +function useMouse() { + const x = value(0) + const y = value(0) + const update = e => { + x.value = e.pageX + y.value = e.pageY + } + onMounted(() => { + window.addEventListener('mousemove', update) + }) + onUnmounted(() => { + window.removeEventListener('mousemove', update) + }) + return { x, y } +} + +// in consuming component +const Component = { + setup() { + const { x, y } = useMouse() + const { z } = useOtherLogic() + return { x, y, z } + }, + template: `
{{ x }} {{ y }} {{ z }}
` +} +``` + +Note in the example above: + +- Properties exposed to the template have clear sources since they are values returned from composition functions; +- Returned values from composition functions can be arbitrarily named so there is no namespace collision; +- There are no unnecessary component instances created just for logic reuse purposes. + +See also: + +- [Appendix: Comparison with React Hooks](#comparison-with-react-hooks) + +## Type Inference + +One of the major goals of 3.0 is to provide better built-in TypeScript type inference support. Originally we tried to address this problem with the now-abandoned [Class API RFC](https://github.com/vuejs/rfcs/pull/17), but after discussion and prototyping we discovered that using Classes [doesn't fully address the typing issue](#type-issues-with-class-api). + +The function-based APIs, on the other hand, are naturally type-friendly. In the prototype we have already achieved full typing support for the proposed APIs. + +See also: + +- [Appendix: Type Issues with Class API](#type-issues-with-class-api) + +## Bundle Size + +Function-based APIs are exposed as named ES exports and imported on demand. This makes them tree-shakable, and leaves more room for future API additions. Code written with function-based APIs also compresses better than object-or-class-based code, since (with standard minification) function and variable names can be shortened while object/class methods and properties cannot. + +# Detailed design + +## The `setup` function + +A new component option, `setup()` is introduced. As the name suggests, this is the place where we use the function-based APIs to setup the logic of our component. `setup()` is called when an instance of the component is created, after props resolution. The function receives the resolved props as its argument: + +``` js +const MyComponent = { + props: { + name: String + }, + setup(props) { + console.log(props.name) + } +} +``` + +Note this `props` object is reactive - i.e. it is updated when new props are passed in, and can be observed and reacted upon using the `watch` function introduced later in this RFC. However, for userland code, it is immutable during development (will emit warning if user code attempts to mutate it). + +`this` is usable inside `setup()`, but you most likely won't need it very often. + +## State + +Similar to `data()`, `setup()` can return an object containing properties to be exposed to the template's render context: + +``` js +const MyComponent = { + props: { + name: String + }, + setup(props) { + return { + msg: `hello ${props.name}!` + } + }, + template: `
{{ msg }}
` +} +``` + +This works exactly like `data()` - `msg` becomes a reactive and mutable property, but **only on the render context.** In order to expose a reactive value that can be mutated by a function declared inside `setup()`, we can use the `value` API: + +``` js +import { value } from 'vue' + +const MyComponent = { + setup(props) { + const msg = value('hello') + const appendName = () => { + msg.value = `hello ${props.name}` + } + return { + msg, + appendName + } + }, + template: `
{{ msg }}
` +} +``` + +Calling `value()` returns a **value wrapper** object that contains a single reactive property: `.value`. This property points to the actual value the wrapper is holding - in the example above, a string. The value can be mutated: + + ``` js +// read the value +console.log(msg.value) // 'hello' + // mutate the value +msg.value = 'bye' +``` + +### Why do we need value wrappers? + +Primitive values in JavaScript like numbers and strings are not passed by reference. Returning a primitive value from a function means the receiving function will not be able to read the latest value when the original is mutated or replaced. + +Value wrappers are important because they provide a way to pass around mutable and reactive references for arbitrary value types. This is what enables composition functions to encapsulate the logic that manages the state while passing the state back to the components as a trackable reference: + +``` js +setup() { + const valueA = useLogicA() // logic inside useLogicA may mutate valueA + const valueB = useLogicB() + return { + valueA, + valueB + } +} +``` + +Value wrappers can also hold non-primitive values and will make all nested properties reactive. Holding non-primitive values like objects and arrays inside a value wrapper provides the ability to entirely replace the value with a fresh one: + +``` js +const numbers = value([1, 2, 3]) +// replace the array with a filtered copy +numbers.value = numbers.value.filter(n => n > 1) +``` + +If you want to create a non-wrapped reactive object, use `state` (which is an exact equivalent of 2.x `Vue.observable` API): + +``` js +import { state } from 'vue' + +const object = state({ + count: 0 +}) + +object.count++ +``` + +### Value Unwrapping + +Note in the last example we are using `{{ msg }}` in the template without the `.value` property access. This is because **value wrappers get "unwrapped" when they are accessed on the render context or as a nested property inside a reactive object.** + +You can mutate an unwrapped value binding in inline handlers: + +``` js +const MyComponent = { + setup() { + return { + count: value(0) + } + }, + template: `` +} +``` + +Value wrappers are also automatically unwrapped when accessed as a nested property inside a reactive object: + +``` js +const count = value(0) +const obj = observable({ + count +}) + +console.log(obj.count) // 0 + +obj.count++ +console.log(obj.count) // 1 +console.log(count.value) // 1 + +count.value++ +console.log(obj.count) // 2 +console.log(count.value) // 2 +``` + +As a rule of thumb, the only occasions where you need to use `.value` is when directly accessing value wrappers as variables. + +### Computed Values + +In addition to plain value wrappers, we can also create computed values: + + ``` js +import { value, computed } from 'vue' + +const count = value(0) +const countPlusOne = computed(() => count.value + 1) + +console.log(countPlusOne.value) // 1 + +count.value++ +console.log(countPlusOne.value) // 2 +``` + +A computed value behaves just like a 2.x computed property: it tracks its dependencies and only re-evaluates when dependencies have changed. + +Computed values can also be returned from `setup()` and will get unwrapped just like normal value wrappers. The main difference is that they are read-only by default - assigning to a computed value's `.value` property or attempting to mutate a computed value binding on the render context will be a no-op and result in a warning. + +To create a writable computed value, provide a setter via the second argument: + +``` js +const count = value(0) +const writableComputed = computed( + // read + () => count.value + 1, + // write + val => { + count.value = val - 1 + } +) +``` + +## Watchers + +The `watch` API provides a way to perform side effect based on reactive state changes. + +The first argument passed to `watch` is called a "source", which can be either a getter function, a value wrapper, or an array containing either. The second argument is a callback that will only get called when the value returned from the getter or the value wrapper has changed: + +``` js +watch( + // getter + () => count.value + 1, + // callback + (value, oldValue) => { + console.log('count + 1 is: ', value) + } +) +// -> count + 1 is: 1 + +count.value++ +// -> count + 1 is: 2 +``` + +Unlike 2.x `$watch`, the callback will be called once when the watcher is first created. This is similar to 2.x watchers with `immediate: true`, but with a slight difference. **By default, the callback is called after current renderer flush.** In other words, the callback is always called when the DOM has already been updated. [This behavior can be configured](#watcher-callback-timing). + +In 2.x we often notice code that performs the same logic in `mounted` and in a watcher callback - e.g. fetching data based on a prop. The new `watch` behavior makes it achievable with a single statement. + +### Watching Props + +As mentioned previously, the `props` object passed to the `setup()` function is reactive and can be used to watch for props changes: + +``` js +const MyComponent = { + props: { + id: number + }, + setup(props) { + const data = value(null) + watch(() => props.id, async (id) => { + data.value = await fetchData(id) + }) + } +} +``` + +### Watching Value Wrappers + +As mentioned, `watch` can watch a value wrapper directly. + +``` js +// double is a computed value +const double = computed(() => count.value * 2) + +// watch a value directly +watch(double, value => { + console.log('double the count is: ', value) +}) // -> double the count is: 0 + +count.value++ // -> double the count is: 2 +``` + +### Watching Multiple Sources + +`watch` can also watch an array of sources. Each source can be either a getter function or a value wrapper. The callback receives an array containing the resolved value for each source: + +``` js +watch( + [valueA, () => valueB.value], + ([a, b], [prevA, prevB]) => { + console.log(`a is: ${a}`) + console.log(`b is: ${b}`) + } +) +``` + +### Stopping a Watcher + +A `watch` call returns a stop handle: + +``` js +const stop = watch(...) +// stop watching +stop() +``` + +If `watch` is called inside `setup()` or lifecycle hooks of a component instance, it will automatically be stopped when the associated component instance is unmounted: + +``` js +export default { + setup() { + // stopped automatically when the component unmounts + watch(/* ... */) + } +} +``` + +### Effect Cleanup + +Sometimes the watcher callback will perform async side effects that need to be invalidated when the watched value changes. The watcher callback receives a 3rd argument that can be used to register a cleanup function. The cleanup function is called when: + +- the watcher is about to re-run +- the watcher is stopped (i.e. when the component is unmounted if `watch` is used inside `setup()`) + +``` js +watch(idValue, (id, oldId, onCleanup) => { + const token = performAsyncOperation(id) + onCleanup(() => { + // id has changed or watcher is stopped. + // invalidate previously pending async operation + token.cancel() + }) +}) +``` + +We are registering cleanup via a passed-in function instead of returning it from the callback (like React `useEffect`) because the return value is important for async error handling. It is very common for the watcher callback to be an async function when performing data fetching: + +``` js +const data = value(null) +watch(getId, async (id) => { + data.value = await fetchData(id) +}) +``` + +An async function implicitly returns a Promise, but the cleanup function needs to be registered immediately before the Promise resolves. In addition, Vue relies on the returned Promise to automatically handle potential errors in the Promise chain. + +### Watcher Callback Timing + +By default, all watcher callbacks are fired **after current renderer flush.** This ensures that when callbacks are fired, the DOM will be in already-updated state. If you want a watcher callback to fire before flush or synchronously, you can use the `flush` option: + +``` js +watch( + () => count.value + 1, + () => console.log(`count changed`), + { + flush: 'post', // default, fire after renderer flush + flush: 'pre', // fire right before renderer flush + flush: 'sync' // fire synchronously + } +) +``` + +### Full `watch` Options + +``` ts +interface WatchOptions { + lazy?: boolean + deep?: boolean + flush?: 'pre' | 'post' | 'sync' + onTrack?: (e: DebuggerEvent) => void + onTrigger?: (e: DebuggerEvent) => void +} + +interface DebuggerEvent { + effect: ReactiveEffect + target: any + key: string | symbol | undefined + type: 'set' | 'add' | 'delete' | 'clear' | 'get' | 'has' | 'iterate' +} +``` + +- `lazy` is the opposite of 2.x's `immediate` option. +- `deep` works the same as 2.x +- `onTrack` and `onTrigger` are hooks that will be called when a dependency is tracked or the watcher getter is triggered. They receive a debugger event that contains information on the operation that caused the track / trigger. + +## Lifecycle Hooks + +All current lifecycle hooks will have an equivalent `useXXX` function that can be used inside `setup()`: + +``` js +import { onMounted, onUpdated, onUnmounted } from 'vue' + +const MyComponent = { + setup() { + onMounted(() => { + console.log('mounted!') + }) + onUpdated(() => { + console.log('updated!') + }) + onUnmounted(() => { + console.log('unmounted!') + }) + } +} +``` + +## Dependency Injection + +``` js +import { provide, inject } from 'vue' + +const CountSymbol = Symbol() + +const Ancestor = { + setup() { + // providing a value can make it reactive + const count = value(0) + provide({ + [CountSymbol]: count + }) + } +} + +const Descendent = { + setup() { + const count = inject(CountSymbol) + return { + count + } + } +} +``` + +If provided key contains a value wrapper, `inject` will also return a value wrapper and the binding will be reactive (i.e. the child will update if ancestor mutates the provided value). + +# Drawbacks + +- Makes it more difficult to reflect and manipulate component definitions. + + This might be a good thing since reflecting and manipulation of component options is usually fragile and risky in a userland context, and creates many edge cases for the runtime to handle (especially when extending or using mixins). The flexibility of function APIs should be able to achieve the same end goals with more explicit userland code. + +- Undisciplined users may end up with "spaghetti code" since they are no longer forced to separate component code into option groups. + + // TODO + +# Alternatives + +- [Class API](https://github.com/vuejs/rfcs/pull/17) (dropped) + +# Adoption strategy + +The proposed APIs are all new additions and can theoretically be introduced in a completely backwards compatible way. However, the new APIs can replace many of the existing options and makes them unnecessary in the long run. Being able to drop some of these old options will result in considerably smaller bundle size and better performance. + +Therefore we are planning to provide two builds for 3.0: + +- **Compatibility build**: supports both the new function-based APIs AND all the 2.x options. + +- **Standard build**: supports the new function-based APIs and only a subset of 2.x options. + +In the compatibility build, `setup()` can be used alongside 2.x options. Note that `setup()` will be called before `data`, `computed` and `method` options are resolved - i.e. you can access values returned from `setup()` on `this` in these options, but not the other way around. + +Current 2.x users can start with the compatibility build and progressively migrate away from deprecated options, until eventually switching to the standard build. + +### Preserved Options + +> Preserved options work the same as 2.x and are available in both the compatibility and standard builds of 3.0. Options marked with * may receive further adjustments before 3.0 official release. + +- `name` +- `props` +- `template` +- `render` +- `components` +- `directives` +- `filters` * +- `delimiters` * +- `comments` * + +### Options deprecated by this RFC + +> These options will only be available in the compatibility build of 3.0. + +- `data` (replaced by `setup()`) +- `computed` (replaced by `computed` returned from `setup()`) +- `methods` (replaced by plain functions returned from `setup()`) +- `watch` (replaced by `watch`) +- `provide/inject` (replaced by `provide` and `inject`) +- `mixins` (replaced by function composition) +- `extends` (replaced by function composition) +- All lifecycle hooks (replaced by `onXXX` functions) + +### Options deprecated by other RFCs + +> These options will only be available in the compatibility build of 3.0. + +- `el` + + Components are no longer mounted by instantiating a constructor with `new`, Instead, a root app instance is created and explicitly mounted. See [RFC#29](https://github.com/vuejs/rfcs/blob/global-api-change/active-rfcs/0000-global-api-change.md#mounting-app-instance). + +- `propsData` + + Props for root component can be passed via app instance's `mount` method. See [RFC#29](https://github.com/vuejs/rfcs/blob/global-api-change/active-rfcs/0000-global-api-change.md#mounting-app-instance). + +- `functional` + + Functional components are now declared as plain functions. See [RFC#27](https://github.com/vuejs/rfcs/pull/27). + +- `model` + + No longer necessary with `v-model` arguments. See [RFC#31](https://github.com/vuejs/rfcs/pull/31). + +- `inheritAttrs` + + Deperecated by [RFC#26](https://github.com/vuejs/rfcs/pull/26). + +# Appendix + +## Comparison with React Hooks + +The function based API provides the same level of logic composition capabilities as React Hooks, but with some important differences. Unlike React hooks, the `setup()` function is called only once. This means code using Vue's function APIs are: + +- In general more aligned with the intuitions of idiomatic JavaScript code; +- Not sensitive to call order and can be conditional; +- Not called repeatedly on each render and produce less GC pressure; +- Not subject to the issue where `useEffect` callback may capture stale variables if the user forgets to pass the correct dependency array; +- Not subject to the issue where `useMemo` is almost always needed in order to prevent inline handlers causing over-re-rendering of child components; + +## Type Issues with Class API + +The primary goal of introducing the Class API was to provide an alternative API that comes with better TypeScript inference support. However, the fact that Vue components need to merge properties declared from multiple sources onto a single `this` context creates a bit of a challenge even with a Class-based API. + +One example is the typing of props. In order to merge props onto `this`, we have to either use a generic argument to the component class, or use a decorator. + +Here's an example using generic arguments: + +``` ts +interface Props { + message: string +} + +class App extends Component { + static props = { + message: String + } +} +``` + +Since the interface passed to the generic argument is in type-land only, the user still needs to provide a runtime props declaration for the props proxying behavior on `this`. This double-declaration is redundant and awkward. + +We've considered using decorators as an alternative: + +``` ts +class App extends Component { + @prop message: string +} +``` + +Using decorators creates a reliance on a stage-2 spec with a lot of uncertainties, especially when TypeScript's current implementation is completely out of sync with the TC39 proposal. In addition, there is no way to expose the types of props declared with decorators on `this.$props`, which breaks TSX support. Users may also assume they can declare a default value for the prop with `@prop message: string = 'foo'` when technically it just can't be made to work as expected. + +In addition, currently there is no way to leverage contextual typing for the arguments of class methods - which means the arguments passed to a Class' `render` function cannot have inferred types based on the Class' other properties. From 24499994804f0f0ec28cba413d09e1b537f38a00 Mon Sep 17 00:00:00 2001 From: Evan You Date: Sat, 8 Jun 2019 00:16:11 +0800 Subject: [PATCH 02/32] more details on spaghetti code --- active-rfcs/0000-function-api.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/active-rfcs/0000-function-api.md b/active-rfcs/0000-function-api.md index bf825cc2..9f8b500d 100644 --- a/active-rfcs/0000-function-api.md +++ b/active-rfcs/0000-function-api.md @@ -515,7 +515,17 @@ If provided key contains a value wrapper, `inject` will also return a value wrap - Undisciplined users may end up with "spaghetti code" since they are no longer forced to separate component code into option groups. - // TODO + I've seen this concern raised a few times in the Class API thread and internally. However, I believe this fear is unwarranted. It is true that the flexibility of function-based API will theoretically allow users to write code that is harder to follow. But let me explain why this is unlikely to happen. + + The biggest difference of function-based APIs vs. the current option-based API is that function APIs make it ridiculously easy to extract part of your component logic into a well encapsulated function. This can be done not just for reuse, but purely for code organization purposes as well. + + With component options, your code only *seem* to be organized - in a complex component, logic related to a specific task is often split up between multiple options. For example, fetching a piece of data often involves one or more properties in `props` and `data()`, a `mounted` hook, and a watcher declared in `watch`. If you put all the logic of your app in a single component, that component is going to become a monster and become very hard to maintain, because every single logical task will be fragmented and spanning across multiple option blocks. + + In comparison, with function-based API all the code related to fetching a specific piece of data can be nicely encapsulated in a single function. + + To avoid the "monster component" problem, we split the component into many smaller ones. Similarly, if you have a huge `setup()` function, you can split it into multiple functions, each dealing with a specific logical task. Function based API makes better organized code easily possible while with options you are stuck with... options (because splitting into mixins makes things worse). + + From this perspective, separation of options vs. `setup()` is just like the separation of HTML/CSS/JS vs. Single File Components. # Alternatives From 307570a1158795d31bc6c9760e4f2e7dc2e00b33 Mon Sep 17 00:00:00 2001 From: Evan You Date: Sat, 8 Jun 2019 11:05:54 +0800 Subject: [PATCH 03/32] tweaks --- active-rfcs/0000-function-api.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/active-rfcs/0000-function-api.md b/active-rfcs/0000-function-api.md index 9f8b500d..fee607e1 100644 --- a/active-rfcs/0000-function-api.md +++ b/active-rfcs/0000-function-api.md @@ -55,7 +55,7 @@ One of the key aspects of the component API is how to encapsulate and reuse logi - Higher-order components (HOCs) - Renderless components (via scoped slots) -These patterns are discussed in more details in the [appendix](#prior-art-composition-patterns) - but in general, they all suffer from one or more of the drawbacks below: +There are plenty of information regarding these patterns on the internet, so we shal not repeat them in full details here. In general, these patterns all suffer from one or more of the drawbacks below: - Unclear sources for properties exposed on the render context. For example, when reading the template of a component using multiple mixins, it can be difficult to tell from which mixin a specific property was injected from. @@ -243,7 +243,7 @@ Value wrappers are also automatically unwrapped when accessed as a nested proper ``` js const count = value(0) -const obj = observable({ +const obj = state({ count }) @@ -458,7 +458,7 @@ interface DebuggerEvent { ## Lifecycle Hooks -All current lifecycle hooks will have an equivalent `useXXX` function that can be used inside `setup()`: +All current lifecycle hooks will have an equivalent `onXXX` function that can be used inside `setup()`: ``` js import { onMounted, onUpdated, onUnmounted } from 'vue' From 21fdc18b923c5da78eaffbc912fdde5695c49bcf Mon Sep 17 00:00:00 2001 From: Evan You Date: Sun, 9 Jun 2019 11:53:16 +0800 Subject: [PATCH 04/32] note on type --- active-rfcs/0000-function-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/active-rfcs/0000-function-api.md b/active-rfcs/0000-function-api.md index fee607e1..62abe253 100644 --- a/active-rfcs/0000-function-api.md +++ b/active-rfcs/0000-function-api.md @@ -107,7 +107,7 @@ See also: One of the major goals of 3.0 is to provide better built-in TypeScript type inference support. Originally we tried to address this problem with the now-abandoned [Class API RFC](https://github.com/vuejs/rfcs/pull/17), but after discussion and prototyping we discovered that using Classes [doesn't fully address the typing issue](#type-issues-with-class-api). -The function-based APIs, on the other hand, are naturally type-friendly. In the prototype we have already achieved full typing support for the proposed APIs. +The function-based APIs, on the other hand, are naturally type-friendly. In the prototype we have already achieved full typing support for the proposed APIs. The best part is - code written in TypeScript will look almost identical to code written in plain JavaScript. There are no manual type hints needed except for dependency injection. See also: From fa1cdf00ae54e4a73e5fcbac965dce6ac61cee35 Mon Sep 17 00:00:00 2001 From: Evan You Date: Sun, 9 Jun 2019 17:22:39 +0800 Subject: [PATCH 05/32] useMemo -> useCallback --- active-rfcs/0000-function-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/active-rfcs/0000-function-api.md b/active-rfcs/0000-function-api.md index 62abe253..d53f1903 100644 --- a/active-rfcs/0000-function-api.md +++ b/active-rfcs/0000-function-api.md @@ -606,7 +606,7 @@ The function based API provides the same level of logic composition capabilities - Not sensitive to call order and can be conditional; - Not called repeatedly on each render and produce less GC pressure; - Not subject to the issue where `useEffect` callback may capture stale variables if the user forgets to pass the correct dependency array; -- Not subject to the issue where `useMemo` is almost always needed in order to prevent inline handlers causing over-re-rendering of child components; +- Not subject to the issue where `useCallback` is almost always needed in order to prevent inline handlers causing over-re-rendering of child components; ## Type Issues with Class API From 94583cc600dd0fce954a021296941c3ba207b56f Mon Sep 17 00:00:00 2001 From: Evan You Date: Sun, 9 Jun 2019 17:27:33 +0800 Subject: [PATCH 06/32] tweaks regarding deps --- active-rfcs/0000-function-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/active-rfcs/0000-function-api.md b/active-rfcs/0000-function-api.md index d53f1903..bd7c9857 100644 --- a/active-rfcs/0000-function-api.md +++ b/active-rfcs/0000-function-api.md @@ -605,8 +605,8 @@ The function based API provides the same level of logic composition capabilities - In general more aligned with the intuitions of idiomatic JavaScript code; - Not sensitive to call order and can be conditional; - Not called repeatedly on each render and produce less GC pressure; -- Not subject to the issue where `useEffect` callback may capture stale variables if the user forgets to pass the correct dependency array; - Not subject to the issue where `useCallback` is almost always needed in order to prevent inline handlers causing over-re-rendering of child components; +- Not subject to the issue where `useEffect` and `useMemo` may capture stale variables if the user forgets to pass the correct dependency array. Vue's automated dependency tracking ensures watchers and computed values are always correctly invalidated. ## Type Issues with Class API From 3247c701dfa95e4852637eeeedc627145bb50df6 Mon Sep 17 00:00:00 2001 From: Evan You Date: Sun, 9 Jun 2019 17:44:32 +0800 Subject: [PATCH 07/32] fix typo --- active-rfcs/0000-function-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/active-rfcs/0000-function-api.md b/active-rfcs/0000-function-api.md index bd7c9857..054d24ef 100644 --- a/active-rfcs/0000-function-api.md +++ b/active-rfcs/0000-function-api.md @@ -55,7 +55,7 @@ One of the key aspects of the component API is how to encapsulate and reuse logi - Higher-order components (HOCs) - Renderless components (via scoped slots) -There are plenty of information regarding these patterns on the internet, so we shal not repeat them in full details here. In general, these patterns all suffer from one or more of the drawbacks below: +There are plenty of information regarding these patterns on the internet, so we shall not repeat them in full details here. In general, these patterns all suffer from one or more of the drawbacks below: - Unclear sources for properties exposed on the render context. For example, when reading the template of a component using multiple mixins, it can be difficult to tell from which mixin a specific property was injected from. From bcc0e2bcf7803c0d2ed63416bb8a11b3883eb2b4 Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 10 Jun 2019 09:54:10 +0800 Subject: [PATCH 08/32] add details on type inference --- active-rfcs/0000-function-api.md | 100 ++++++++++++++++++++++++++++++- 1 file changed, 99 insertions(+), 1 deletion(-) diff --git a/active-rfcs/0000-function-api.md b/active-rfcs/0000-function-api.md index 054d24ef..ce94074e 100644 --- a/active-rfcs/0000-function-api.md +++ b/active-rfcs/0000-function-api.md @@ -107,7 +107,7 @@ See also: One of the major goals of 3.0 is to provide better built-in TypeScript type inference support. Originally we tried to address this problem with the now-abandoned [Class API RFC](https://github.com/vuejs/rfcs/pull/17), but after discussion and prototyping we discovered that using Classes [doesn't fully address the typing issue](#type-issues-with-class-api). -The function-based APIs, on the other hand, are naturally type-friendly. In the prototype we have already achieved full typing support for the proposed APIs. The best part is - code written in TypeScript will look almost identical to code written in plain JavaScript. There are no manual type hints needed except for dependency injection. +The function-based APIs, on the other hand, are naturally type-friendly. In the prototype we have already achieved full typing support for the proposed APIs. The best part is - code written in TypeScript will look almost identical to code written in plain JavaScript. More details will be discussed later in this RFC. See also: @@ -507,6 +507,104 @@ const Descendent = { If provided key contains a value wrapper, `inject` will also return a value wrapper and the binding will be reactive (i.e. the child will update if ancestor mutates the provided value). +## Type Inference + +To get proper type inference in TypeScript, we do need to wrap a component definition in a function call: + +``` ts +import { createComponent } from 'vue' + +const MyComponent = createComponent({ + props: { + msg: String + }, + setup(props) { + watch(() => props.msg, msg => { /* ... */ }) + return { + count: value(0) + } + }, + render({ state, props }) { + // state typing inferred from value returned by setup() + console.log(state.count) + // props typing inferred from `props` declaration + console.log(props.msg) + + // `this` exposes both state and props + console.log(this.count) + console.log(this.msg) + } +}) +``` + +`createComponent` is conceptually similar to 2.x's `Vue.extend`, but internally it's just a no-op for typing purposes. The returned component is the object itself, but typed in a way that would provide props inference when used in TSX expressions. + +If you are using Single-File Components, Vetur can implicitly add the wrapper function for you. + +### Required Props + +By default, props are inferred as optional properties. `required: true` will be respected if present: + +``` ts +import { createComponent } from 'vue' + +createComponent({ + props: { + foo: { + type: String, + required: true + }, + 'bar: { + type: String + } + } as const, + setup(props) { + props.foo // string + props.bar // string | undefined + } +}) +``` + +Note that we need to add `as const` after the `props` declaration. This is because without `as const` the type will be `required: boolean` and won't qualify for `extends true` in conditional type operations. + +> Side note: should we consider making props required by default (And can be made optional with `optional: true`)? + +### Complex Prop Types + +The exposed `PropType` type can be used to declare complex prop types - but it requires a force-cast via `as any`: + +``` ts +import { createComponent, PropType } from 'vue' + +createComponent({ + props: { + options: { + type: (null as any) as PropType<{ msg: string }>, + } + }, + setup(props) { + props.options // { msg: string } | undefined + } +}) +``` + +### Dependency Injection Typing + +The `inject` method is the only API that requires manual typing: + +``` ts +import { createComponent, inject, Value } from 'vue' + +createComponent({ + setup() { + const count: Value = inject(CountSymbol) + return { + count + } + } +}) +``` + # Drawbacks - Makes it more difficult to reflect and manipulate component definitions. From 879e36757093091fe92386b609a1da30e642f229 Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 10 Jun 2019 19:26:50 +0800 Subject: [PATCH 09/32] small edits --- active-rfcs/0000-function-api.md | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/active-rfcs/0000-function-api.md b/active-rfcs/0000-function-api.md index ce94074e..5a54f450 100644 --- a/active-rfcs/0000-function-api.md +++ b/active-rfcs/0000-function-api.md @@ -298,7 +298,13 @@ const writableComputed = computed( The `watch` API provides a way to perform side effect based on reactive state changes. -The first argument passed to `watch` is called a "source", which can be either a getter function, a value wrapper, or an array containing either. The second argument is a callback that will only get called when the value returned from the getter or the value wrapper has changed: +The first argument passed to `watch` is called a "source", which can be one of the following: + +- a getter function +- a value wrapper +- an array containing the two above types + +The second argument is a callback that will only get called when the value returned from the getter or the value wrapper has changed: ``` js watch( @@ -578,9 +584,7 @@ import { createComponent, PropType } from 'vue' createComponent({ props: { - options: { - type: (null as any) as PropType<{ msg: string }>, - } + options: (null as any) as PropType<{ msg: string }> }, setup(props) { props.options // { msg: string } | undefined @@ -605,6 +609,8 @@ createComponent({ }) ``` +Here `Value` is the exposed type for value wrappers - it accepts a generic argument to represent the type of its internal value. + # Drawbacks - Makes it more difficult to reflect and manipulate component definitions. @@ -661,7 +667,7 @@ Current 2.x users can start with the compatibility build and progressively migra > These options will only be available in the compatibility build of 3.0. -- `data` (replaced by `setup()`) +- `data` (replaced by `setup()` + `value` + `state`) - `computed` (replaced by `computed` returned from `setup()`) - `methods` (replaced by plain functions returned from `setup()`) - `watch` (replaced by `watch`) @@ -706,6 +712,8 @@ The function based API provides the same level of logic composition capabilities - Not subject to the issue where `useCallback` is almost always needed in order to prevent inline handlers causing over-re-rendering of child components; - Not subject to the issue where `useEffect` and `useMemo` may capture stale variables if the user forgets to pass the correct dependency array. Vue's automated dependency tracking ensures watchers and computed values are always correctly invalidated. +> Note: we acknowledge the creativity of React Hooks, and it is a major source of inspiration for this proposal. However, the issues mentioned above do exist in its design and we noticed Vue's reactivity model happens to provide a way around them. + ## Type Issues with Class API The primary goal of introducing the Class API was to provide an alternative API that comes with better TypeScript inference support. However, the fact that Vue components need to merge properties declared from multiple sources onto a single `this` context creates a bit of a challenge even with a Class-based API. From 2cd74d70b99f794e64aada087e3104a606c90efc Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 10 Jun 2019 19:32:24 +0800 Subject: [PATCH 10/32] fix typos --- active-rfcs/0000-function-api.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/active-rfcs/0000-function-api.md b/active-rfcs/0000-function-api.md index 5a54f450..d5eb93ea 100644 --- a/active-rfcs/0000-function-api.md +++ b/active-rfcs/0000-function-api.md @@ -332,7 +332,7 @@ As mentioned previously, the `props` object passed to the `setup()` function is ``` js const MyComponent = { props: { - id: number + id: Number }, setup(props) { const data = value(null) @@ -560,7 +560,7 @@ createComponent({ type: String, required: true }, - 'bar: { + bar: { type: String } } as const, From 3ada0dcc829e22298374c573ce0456b0cd64c412 Mon Sep 17 00:00:00 2001 From: Evan You Date: Tue, 11 Jun 2019 23:06:47 +0800 Subject: [PATCH 11/32] add an example for render function usage --- active-rfcs/0000-function-api.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/active-rfcs/0000-function-api.md b/active-rfcs/0000-function-api.md index d5eb93ea..ea85360c 100644 --- a/active-rfcs/0000-function-api.md +++ b/active-rfcs/0000-function-api.md @@ -260,6 +260,31 @@ console.log(count.value) // 2 As a rule of thumb, the only occasions where you need to use `.value` is when directly accessing value wrappers as variables. +### Bindings in Manual Render Functions + +Bindings returned by `setup()` is also usable inside manually written render functions - they are exposed on `this`. In addition, the render function receives the internal component instance as the argument, from which `state`, `props` and `slots` can be destructured: + +``` js +const MyComponent = { + props: { + msg: String + }, + setup() { + return { + count: value(0) + } + }, + render({ state, props, slots }) { + // `state` contains bindings returned from setup() + console.log(state.count) + console.log(props.msg) + // `this` exposes both state and props + console.log(this.count) + console.log(this.msg) + } +} +``` + ### Computed Values In addition to plain value wrappers, we can also create computed values: From 05530ec8168993c1a788fafa4532bd9790e9847f Mon Sep 17 00:00:00 2001 From: Evan You Date: Wed, 12 Jun 2019 14:58:34 +0800 Subject: [PATCH 12/32] improve render fn usage & type inference details --- active-rfcs/0000-function-api.md | 104 ++++++++++++++++++++++--------- 1 file changed, 73 insertions(+), 31 deletions(-) diff --git a/active-rfcs/0000-function-api.md b/active-rfcs/0000-function-api.md index ea85360c..a5075308 100644 --- a/active-rfcs/0000-function-api.md +++ b/active-rfcs/0000-function-api.md @@ -224,7 +224,7 @@ object.count++ ### Value Unwrapping -Note in the last example we are using `{{ msg }}` in the template without the `.value` property access. This is because **value wrappers get "unwrapped" when they are accessed on the render context or as a nested property inside a reactive object.** +Note in the last example we are using `{{ msg }}` in the template without the `.value` property access. This is because **value wrappers get "unwrapped" when they are accessed in the template or as a nested property inside a reactive object.** You can mutate an unwrapped value binding in inline handlers: @@ -260,31 +260,33 @@ console.log(count.value) // 2 As a rule of thumb, the only occasions where you need to use `.value` is when directly accessing value wrappers as variables. -### Bindings in Manual Render Functions +### Usage with Manual Render Functions -Bindings returned by `setup()` is also usable inside manually written render functions - they are exposed on `this`. In addition, the render function receives the internal component instance as the argument, from which `state`, `props` and `slots` can be destructured: +If the component doesn't use a template, `setup()` can also directly return a render function instead: ``` js +import { value, createElement as h } from 'vue' + const MyComponent = { - props: { - msg: String - }, - setup() { - return { - count: value(0) - } - }, - render({ state, props, slots }) { - // `state` contains bindings returned from setup() - console.log(state.count) - console.log(props.msg) - // `this` exposes both state and props - console.log(this.count) - console.log(this.msg) + setup(initialProps) { + const count = value(0) + const increment = () => { count.value++ } + + return (props, slots, attrs, vnode) => ( + h('button', { + onClick: increment + }, count.value) + ) } } ``` +The returned render function has the same signature as specified in [RFC#28](https://github.com/vuejs/rfcs/pull/28). + +You may notice that both `setup()` and the returned function receive props as the first argument. They work mostly the same, but the `props` passed to the render function is a plain object in production and offers better performance. + +A normal `render()` option is still available, but is mostly used as a result of template compilation. For manual render functions, an inline function returned from `setup()` should be preferred since it avoids the need for proxying bindings and makes type inference easier. + ### Computed Values In addition to plain value wrappers, we can also create computed values: @@ -546,31 +548,71 @@ To get proper type inference in TypeScript, we do need to wrap a component defin import { createComponent } from 'vue' const MyComponent = createComponent({ + // props declarations are used to infer prop types props: { msg: String }, setup(props) { - watch(() => props.msg, msg => { /* ... */ }) + props.msg // string | undefined + + // bindings returned from setup() can be used for type inference + // in templates + const count = value(0) return { - count: value(0) + count } + } +}) +``` + +`createComponent` is conceptually similar to 2.x's `Vue.extend`, but it is a no-op and only needed for typing purposes. The returned component is the object itself, but typed in a way that would provide type information for Vetur and TSX. If you are using Single-File Components, Vetur can implicitly add the wrapper function for you. + +If you are using render functions / TSX, returning a render function inside `setup()` provides proper type support (again, no manual type hints needed): + +``` ts +import { createComponent, createElement as h } from 'vue' + +const MyComponent = createComponent({ + props: { + msg: String }, - render({ state, props }) { - // state typing inferred from value returned by setup() - console.log(state.count) - // props typing inferred from `props` declaration - console.log(props.msg) - - // `this` exposes both state and props - console.log(this.count) - console.log(this.msg) + setup(props) { + const count = value(0) + return () => h('div', [ + h('p', `msg is ${props.msg}`), + h('p', `count is ${count.value}`) + ]) + } +}) +``` + +### TypeScript-only Props Typing + +In 3.0, the `props` declaration is optional. If you don't want runtime props validation, you can omit `props` declaration and declare your expected prop types directly in TypeScript: + +``` ts +import { createComponent, createElement as h } from 'vue' + +interface Props { + msg: string +} + +const MyComponent = createComponent({ + setup(props: Props) { + return () => h('div', props.msg) } }) ``` -`createComponent` is conceptually similar to 2.x's `Vue.extend`, but internally it's just a no-op for typing purposes. The returned component is the object itself, but typed in a way that would provide props inference when used in TSX expressions. +You can even pass the setup function directly if you don't need any other options: + +``` ts +const MyComponent = createComponent((props: { msg: string }) => { + return () => h('div', props.msg) +}) +``` -If you are using Single-File Components, Vetur can implicitly add the wrapper function for you. +The returned `MyComponent` also provides type inference when used in TSX. ### Required Props From adefea69995e0c18270f4032b0997375ae8d205d Mon Sep 17 00:00:00 2001 From: Evan You Date: Wed, 12 Jun 2019 22:26:43 +0800 Subject: [PATCH 13/32] Update 0000-function-api.md --- active-rfcs/0000-function-api.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/active-rfcs/0000-function-api.md b/active-rfcs/0000-function-api.md index a5075308..4721d02a 100644 --- a/active-rfcs/0000-function-api.md +++ b/active-rfcs/0000-function-api.md @@ -680,13 +680,13 @@ Here `Value` is the exposed type for value wrappers - it accepts a generic argum # Drawbacks -- Makes it more difficult to reflect and manipulate component definitions. +### Runtime Reflection of Components - This might be a good thing since reflecting and manipulation of component options is usually fragile and risky in a userland context, and creates many edge cases for the runtime to handle (especially when extending or using mixins). The flexibility of function APIs should be able to achieve the same end goals with more explicit userland code. +The new API makes it more difficult to reflect and manipulate component definitions. This might be a good thing since reflecting and manipulation of component options is usually fragile and risky in a userland context, and creates many edge cases for the runtime to handle (especially when extending or using mixins). The flexibility of function APIs should be able to achieve the same end goals with more explicit userland code. -- Undisciplined users may end up with "spaghetti code" since they are no longer forced to separate component code into option groups. +### Spaghetti Code in Unexperienced Hands - I've seen this concern raised a few times in the Class API thread and internally. However, I believe this fear is unwarranted. It is true that the flexibility of function-based API will theoretically allow users to write code that is harder to follow. But let me explain why this is unlikely to happen. +Some feedbacks suggest that undisciplined users may end up with "spaghetti code" since they are no longer forced to separate component code into option groups. I believe this fear is unwarranted. It is true that the flexibility of function-based API will theoretically allow users to write code that is harder to follow. But let me explain why this is unlikely to happen. The biggest difference of function-based APIs vs. the current option-based API is that function APIs make it ridiculously easy to extract part of your component logic into a well encapsulated function. This can be done not just for reuse, but purely for code organization purposes as well. From 08ad9e1117c96d7529a7d8ba0d98ae70c3620aec Mon Sep 17 00:00:00 2001 From: Evan You Date: Wed, 19 Jun 2019 17:02:33 +0800 Subject: [PATCH 14/32] provide context and disable `this` in setup() --- active-rfcs/0000-function-api.md | 46 ++++++++++++++++++++++++++++++-- 1 file changed, 44 insertions(+), 2 deletions(-) diff --git a/active-rfcs/0000-function-api.md b/active-rfcs/0000-function-api.md index 4721d02a..81d54c68 100644 --- a/active-rfcs/0000-function-api.md +++ b/active-rfcs/0000-function-api.md @@ -121,7 +121,7 @@ Function-based APIs are exposed as named ES exports and imported on demand. This ## The `setup` function -A new component option, `setup()` is introduced. As the name suggests, this is the place where we use the function-based APIs to setup the logic of our component. `setup()` is called when an instance of the component is created, after props resolution. The function receives the resolved props as its argument: +A new component option, `setup()` is introduced. As the name suggests, this is the place where we use the function-based APIs to setup the logic of our component. `setup()` is called when an instance of the component is created, after props resolution. The function receives the resolved props as its first argument: ``` js const MyComponent = { @@ -136,7 +136,49 @@ const MyComponent = { Note this `props` object is reactive - i.e. it is updated when new props are passed in, and can be observed and reacted upon using the `watch` function introduced later in this RFC. However, for userland code, it is immutable during development (will emit warning if user code attempts to mutate it). -`this` is usable inside `setup()`, but you most likely won't need it very often. +The second argument provides a context object which exposes a number of properties that were previously exposed on `this` in 2.x APIs: + +``` js +const MyComponent = { + setup(props, context) { + context.attrs + context.slots + context.refs + context.emit + context.parent + context.root + } +} +``` + +`attrs`, `slots` and `refs` are in fact proxies to the corresponding values on the internal component instance. This ensures they always expose the latest values even after updates, so we can destructure them without worrying accessing a stale reference: + +``` js +const MyComponent = { + setup(props, { refs }) { + // a function that may get called at a later stage + function onClick() { + refs.foo // guaranteed to be the latest reference + } + } +} +``` + +Why don't we expose `props` via context as well, so that `setup()` needs just a single argument? There are several reasons for this: + +- It's much more common for a component to use `props` than the other properties, and very often a component uses only `props`. + +- Having `props` as a separate argument makes it easier to type it individually (see [TypeScript-only Props Typing](#typescript-only-props-typing) below) without messing up the types of other properties on the context. It also makes it possible to keep a consistent signature across `setup`, `render` and plain functional components with TSX support. + +**`this` is not available inside `setup()`.** The reason for avoiding `this` is because of a very common pitfall for beginners: + +``` js +setup() { + function onClick() { + this // not the `this` you'd expect! + } +} +``` ## State From f8b4dfec481f7ce82a1b0f2bf374fde31f086e71 Mon Sep 17 00:00:00 2001 From: Evan You Date: Wed, 19 Jun 2019 17:07:06 +0800 Subject: [PATCH 15/32] improve dependency injection typing --- active-rfcs/0000-function-api.md | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/active-rfcs/0000-function-api.md b/active-rfcs/0000-function-api.md index 81d54c68..be0170c2 100644 --- a/active-rfcs/0000-function-api.md +++ b/active-rfcs/0000-function-api.md @@ -703,14 +703,24 @@ createComponent({ ### Dependency Injection Typing -The `inject` method is the only API that requires manual typing: +`provide` and `inject` can be typed by providing a typed symbol using the `Key` type: ``` ts -import { createComponent, inject, Value } from 'vue' +import { createComponent, provide, inject, Key } from 'vue' -createComponent({ +const CountSymbol: Key = Symbol() + +const Provider = createComponent({ setup() { - const count: Value = inject(CountSymbol) + // will error if provided value is not a number + provide(CountSymbol, 123) + } +}) + +const Consumer = createComponent({ + setup() { + const count = inject(CountSymbol) // count's type is Value + console.log(count.value) // 123 return { count } @@ -718,8 +728,6 @@ createComponent({ }) ``` -Here `Value` is the exposed type for value wrappers - it accepts a generic argument to represent the type of its internal value. - # Drawbacks ### Runtime Reflection of Components From 79c933a4f854cf51e15ec6b189e3371a819a370c Mon Sep 17 00:00:00 2001 From: Guillaume Chau Date: Fri, 21 Jun 2019 02:16:21 +0200 Subject: [PATCH 16/32] use SFC syntax in basic example --- active-rfcs/0000-function-api.md | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/active-rfcs/0000-function-api.md b/active-rfcs/0000-function-api.md index be0170c2..28862569 100644 --- a/active-rfcs/0000-function-api.md +++ b/active-rfcs/0000-function-api.md @@ -9,17 +9,11 @@ Expose logic-related component options via function-based APIs instead. # Basic example -``` js +``` vue + + + ``` # Motivation From b06b89e92e2b69d7a2749eec9757a971a970cddf Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 21 Jun 2019 10:09:56 +0800 Subject: [PATCH 17/32] provide high-level Q&A + more exmaples --- active-rfcs/0000-function-api.md | 339 +++++++++++++++++++++++++++++-- 1 file changed, 321 insertions(+), 18 deletions(-) diff --git a/active-rfcs/0000-function-api.md b/active-rfcs/0000-function-api.md index 28862569..63a2df60 100644 --- a/active-rfcs/0000-function-api.md +++ b/active-rfcs/0000-function-api.md @@ -3,6 +3,44 @@ - Reference Issues: - Implementation PR: (leave this empty) +> If you came here from HN/Reddit, we strongly suggest that you read this RFC in its entirety before making a comment. + +# High Level Q&A + +## Is this like Python 3 / Do I have to rewrite all my code? + +No. The new API is 100% compatible with current syntax and purely additive. All new additions are contained within the new `setup()` function. 3.0 standard build will support 2.x options plus the new APIs, but you can optionally use the lean build which drops a number of options while providing a smaller and faster runtime. [Details](#adoption-strategy) + +2.x options compatibility will be kept through the entire 3.x lifecycle. + +## Is this set in stone? + +No. This is an RFC (Request for Comments) - as long as this pull request is still open, this is just a proposal for soliciting feedback. We encourage you to voice your opinion, but **please actually read the RFC itself before commenting, as the impression you got from a random Reddit/HN thread is likely misleading.** + +## Vue is all about simplicity and this RFC is not. + +RFCs are written for implementors and advanced users who are aware of the internal design constraints of the framework. It focuses on the technical details, and has to be extremely through and cover all possible edge cases, which is why it may seem complex at first glance. + +We will provide tutorials targeting normal users which will be much easier to follow along with. + +## This will lead to spaghetti code and is much harder to read. + +Please read [this section](#spaghetti-code-in-unexperienced-hands). + +Also see [more examples comparing the new API to 2.x options](comparison-with-2.x-api). + +## The Class API is much better! + +We [respectfully](https://github.com/vuejs/rfcs/blob/function-apis/active-rfcs/0000-function-api.md#type-issues-with-class-api) [disagree](https://github.com/vuejs/rfcs/pull/17#issuecomment-494242121). + +This RFC also provide strictly superior logic composition and better type inference than the Class API. As it stands, the only "advantage" the Class API has is familiarity - and we don't believe it's enough to outweigh the benefits this RFC provides over it. + +## This looks like React, why don't I just use React? + +First, you are not forced to use this API at all. + +Second, if you use React, you'll most likely be using React Hooks. This API is certainly inspired by React hooks, but it works fundamentally differently. In fact, [we believe this API addresses a number of important usability issues in React Hooks](#comaprison-with-react-hooks). If you cannot put up with this API, you will most likely hate React Hooks even more. + # Summary Expose logic-related component options via function-based APIs instead. @@ -10,6 +48,14 @@ Expose logic-related component options via function-based APIs instead. # Basic example ``` vue + + - - ``` # Motivation @@ -757,21 +795,21 @@ Some feedbacks suggest that undisciplined users may end up with "spaghetti code" # Adoption strategy -The proposed APIs are all new additions and can theoretically be introduced in a completely backwards compatible way. However, the new APIs can replace many of the existing options and makes them unnecessary in the long run. Being able to drop some of these old options will result in considerably smaller bundle size and better performance. +The proposed APIs are all new additions and can be introduced in a completely backwards compatible way. However, the new APIs can replace many of the existing options and makes them unnecessary in the long run. Being able to drop some of these old options will result in considerably smaller bundle size and better performance. Therefore we are planning to provide two builds for 3.0: -- **Compatibility build**: supports both the new function-based APIs AND all the 2.x options. +- **Standard build**: compatible with 2.x API (except for breaking changes introduced in other RFCs), with the ability to optionally use the new APIs introduced in this RFC. -- **Standard build**: supports the new function-based APIs and only a subset of 2.x options. +- **Lean build**: only supports the new APIs and a subset of 2.x options. If you are starting a fresh project using only the new API, this would give you a smaller and faster runtime. -In the compatibility build, `setup()` can be used alongside 2.x options. Note that `setup()` will be called before `data`, `computed` and `method` options are resolved - i.e. you can access values returned from `setup()` on `this` in these options, but not the other way around. +In the standard build, `setup()` can be used alongside 2.x options. Note that `setup()` will be called before `data`, `computed` and `method` options are resolved - i.e. you can access values returned from `setup()` on `this` in these options, but not the other way around. -Current 2.x users can start with the compatibility build and progressively migrate away from deprecated options, until eventually switching to the standard build. +Current 2.x users can start with the standard build and progressively introduce the new API into their current codebase, without having to do a full migration all at once. -### Preserved Options +### Common Options -> Preserved options work the same as 2.x and are available in both the compatibility and standard builds of 3.0. Options marked with * may receive further adjustments before 3.0 official release. +> Common options work the same as 2.x and are available in both the full and lean builds of 3.0. Options marked with * may receive further adjustments before 3.0 official release. - `name` - `props` @@ -783,9 +821,9 @@ Current 2.x users can start with the compatibility build and progressively migra - `delimiters` * - `comments` * -### Options deprecated by this RFC +### Options removed in the Lean Build -> These options will only be available in the compatibility build of 3.0. +> These options will not be available in the lean build of 3.0. - `data` (replaced by `setup()` + `value` + `state`) - `computed` (replaced by `computed` returned from `setup()`) @@ -798,7 +836,7 @@ Current 2.x users can start with the compatibility build and progressively migra ### Options deprecated by other RFCs -> These options will only be available in the compatibility build of 3.0. +> There are a number of additional options that are deprecated by other RFCs. These options will likely be supported via a compatibility plugin. They are not strictly related to this RFC, but we are listing them here for completeness. - `el` @@ -822,6 +860,271 @@ Current 2.x users can start with the compatibility build and progressively migra # Appendix +## Comparison with 2.x API + +### Simple Counter + +2.x + +``` vue + + + +``` + +New API + +``` vue + + + +``` + +### Fetching Data Based on Prop + +2.x + +``` vue + + + +``` + +New API + +``` vue + + + +``` + +### Multiple Logic Topics + +Based on the previous data-fetching example, suppose we want to also track mouse position in the same component: + +2.x + +``` vue + + + +``` + +You'll start to notice that we have two logic topics (data fetching and mouse position tracking) but they are split up and mixed between component options. + +With new API: + +``` vue + + + +``` + +Notice how the new API cleanly organizes code by logical topic instead of options. + ## Comparison with React Hooks The function based API provides the same level of logic composition capabilities as React Hooks, but with some important differences. Unlike React hooks, the `setup()` function is called only once. This means code using Vue's function APIs are: From 9acf776339f6046ba7ade91d67ce1fa75012a664 Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 21 Jun 2019 10:12:44 +0800 Subject: [PATCH 18/32] fix link --- active-rfcs/0000-function-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/active-rfcs/0000-function-api.md b/active-rfcs/0000-function-api.md index 63a2df60..e58f23ee 100644 --- a/active-rfcs/0000-function-api.md +++ b/active-rfcs/0000-function-api.md @@ -27,7 +27,7 @@ We will provide tutorials targeting normal users which will be much easier to fo Please read [this section](#spaghetti-code-in-unexperienced-hands). -Also see [more examples comparing the new API to 2.x options](comparison-with-2.x-api). +Also see [more examples comparing the new API to 2.x options](#comparison-with-2x-api). ## The Class API is much better! From e32744377da2431bf297673f337947a4ec615072 Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 21 Jun 2019 10:44:31 +0800 Subject: [PATCH 19/32] tweak q&a --- active-rfcs/0000-function-api.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/active-rfcs/0000-function-api.md b/active-rfcs/0000-function-api.md index e58f23ee..55a937e7 100644 --- a/active-rfcs/0000-function-api.md +++ b/active-rfcs/0000-function-api.md @@ -3,9 +3,7 @@ - Reference Issues: - Implementation PR: (leave this empty) -> If you came here from HN/Reddit, we strongly suggest that you read this RFC in its entirety before making a comment. - -# High Level Q&A +# High-level Q&A ## Is this like Python 3 / Do I have to rewrite all my code? @@ -21,14 +19,12 @@ No. This is an RFC (Request for Comments) - as long as this pull request is stil RFCs are written for implementors and advanced users who are aware of the internal design constraints of the framework. It focuses on the technical details, and has to be extremely through and cover all possible edge cases, which is why it may seem complex at first glance. -We will provide tutorials targeting normal users which will be much easier to follow along with. +We will provide tutorials targeting normal users which will be much easier to follow along with. In the meanwhile, check out [some examples](#comparison-with-2x-api) to see if the new API really makes things more complex. ## This will lead to spaghetti code and is much harder to read. Please read [this section](#spaghetti-code-in-unexperienced-hands). -Also see [more examples comparing the new API to 2.x options](#comparison-with-2x-api). - ## The Class API is much better! We [respectfully](https://github.com/vuejs/rfcs/blob/function-apis/active-rfcs/0000-function-api.md#type-issues-with-class-api) [disagree](https://github.com/vuejs/rfcs/pull/17#issuecomment-494242121). From f1000267b2cf15164ec0ee9aade2e2b7bc0ec600 Mon Sep 17 00:00:00 2001 From: brian <734339+btc@users.noreply.github.com> Date: Thu, 20 Jun 2019 22:53:01 -0400 Subject: [PATCH 20/32] Update 0000-function-api.md --- active-rfcs/0000-function-api.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/active-rfcs/0000-function-api.md b/active-rfcs/0000-function-api.md index 55a937e7..578354b9 100644 --- a/active-rfcs/0000-function-api.md +++ b/active-rfcs/0000-function-api.md @@ -17,7 +17,7 @@ No. This is an RFC (Request for Comments) - as long as this pull request is stil ## Vue is all about simplicity and this RFC is not. -RFCs are written for implementors and advanced users who are aware of the internal design constraints of the framework. It focuses on the technical details, and has to be extremely through and cover all possible edge cases, which is why it may seem complex at first glance. +RFCs are written for implementors and advanced users who are aware of the internal design constraints of the framework. It focuses on the technical details, and has to be extremely thorough and cover all possible edge cases, which is why it may seem complex at first glance. We will provide tutorials targeting normal users which will be much easier to follow along with. In the meanwhile, check out [some examples](#comparison-with-2x-api) to see if the new API really makes things more complex. From ba6845d0ff88818b7bd002c4c21ed6e30b728da3 Mon Sep 17 00:00:00 2001 From: Evan You Date: Fri, 21 Jun 2019 11:18:18 +0800 Subject: [PATCH 21/32] tweaks --- active-rfcs/0000-function-api.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/active-rfcs/0000-function-api.md b/active-rfcs/0000-function-api.md index 578354b9..7294530c 100644 --- a/active-rfcs/0000-function-api.md +++ b/active-rfcs/0000-function-api.md @@ -33,9 +33,9 @@ This RFC also provide strictly superior logic composition and better type infere ## This looks like React, why don't I just use React? -First, you are not forced to use this API at all. +First, the template syntax doesn't change, and you are not forced to use this API for your ` ``` -New API +Functions API ``` vue