Giter VIP home page Giter VIP logo

core's Introduction

vuejs/core npm build status Download

Getting Started

Please follow the documentation at vuejs.org!

Sponsors

Vue.js is an MIT-licensed open source project with its ongoing development made possible entirely by the support of these awesome backers. If you'd like to join them, please consider sponsoring Vue's development.

Special Sponsor

special sponsor appwrite

sponsors

Questions

For questions and support please use the official forum or community chat. The issue list of this repo is exclusively for bug reports and feature requests.

Issues

Please make sure to respect issue requirements and use the new issue helper when opening an issue. Issues not conforming to the guidelines may be closed immediately.

Stay In Touch

Contribution

Please make sure to read the Contributing Guide before making a pull request. If you have a Vue-related project/component/tool, add it with a pull request to this curated list!

Thank you to all the people who already contributed to Vue!

License

MIT

Copyright (c) 2013-present, Yuxi (Evan) You

core's People

Contributors

a631807682 avatar akryum avatar alfred-skyblue avatar antfu avatar baiwusanyu-c avatar cexbrayat avatar dependabot-preview[bot] avatar dependabot[bot] avatar dsseng avatar edison1105 avatar hcysunyang avatar himself65 avatar jiangying000 avatar johnsoncodehk avatar linusborg avatar liulinboyi avatar pikax avatar posva avatar renovate[bot] avatar shengxinjing avatar sodatea avatar sxzz avatar unbyte avatar underfin avatar webfansplz avatar yangmingshan avatar ygj6 avatar yyx990803 avatar zhangenming avatar zhangzhonghe avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

core's Issues

Initial proposal of breaking changes in 3.0

Changes

Props

Component no longer need to delcare props in order to receive props. Everything passed from parent vnode's data (with the exception of internal properties, i,e. key, ref, slots and nativeOn*) will be available in this.$props and also as the first argument of the render function. This eliminates the need for this.$attrs and this.$listeners.

When no props are declared on a component, props will not be proxied on the component instance and can only be accessed via this.$props or the props argument in render functions.

You still can delcare props in order to specify default values and perform runtime type checking, and it works just like before. Declared props will also be proxied on the component instance. However, the behavior of undeclared props falling through as attrs will be removed; it's as if inheritAttr now defaults to false. The component will be responsible for merging the props as attrs onto the desired element.

VNodes

Flat Data Format

// before
{
  attrs: { id: 'foo' },
  domProps: { innerHTML: '' },
  on: { click: foo },
  key: 'foo',
  ref: 'bar'
}

// after (consistent with JSX usage)
{
  id: 'foo',
  domPropsInnerHTML: '',
  onClick: foo,
  key: 'foo',
  ref: ref => {
    this.$refs.bar = ref
  }
}
  • Less memory allocation & faster diffs
  • Makes it consistent with JSX
  • Easier spread operations

VNodes are now context-free

h can now be globally imported and is no longer bound to component instacnes. VNodes created are also no longer bound to compomnent instances (this means you can no longer access vnode.context to get the component instance that created it)

Component in Render Functions

No longer resolves component by string names; Any h call with a string is considered an element. Components must be resolved before being passed to h.

import { resolveComponent } from 'vue'

render (h) {
  // only necessary when you are trying to access a registered component instead
  // of an imported one
  const Comp = resolveComponent(this, 'foo')
  return h(Comp)
}

In templates, components should be uppercase to differentiate from normal elements.

NOTE: how to tell in browser templates? In compiler, use the following intuitions:

  1. If uppercase -> Component
  2. If known HTML elements -> element
  3. Treat as unknown component - at runtime, try resolving as a component first, if not found, render as element. (resolveComponent returns name string if component is not found)

Slots

Unifying Normnal Slots and Scoped Slots

Scoped slots and normal slots are now unified. There's no more difference between the two. Inside a component, all slots on this.$slots will be functions and all them can be passed arguments.

Usage Syntax Change

// before
h(Comp, [
  h('div', { slot: 'foo' }, 'foo')
  h('div', { slot: 'bar' }, 'bar')
])

// after
h(Comp, () => h('div', 'default slot'))

// or
import { childFlags } from 'vue/flags'

h(Comp, null, {
  slots: {
    foo: () => h('div', 'foo'),
    bar: () => h('div', 'bar')
  }
}, childFlags.COMPILED_SLOTS)

// also works
h(Comp, null, {
  foo: () => h('div', 'foo'),
  bar: () => h('div', 'bar')
})

Functional Component

Functional components can now really be just functions.

// before
const Func = {
  functional: true,
  render (h, ctx) {
    return h('div')
  }
}

// Now can also be:
const Func = (h, props, slots, ctx) => h('div')
Func.pure = true

Async Component

Async components now must be explicitly created.

import { createAsyncComponent } from 'vue'

const AsyncFoo = createAsyncComponent(() => import('./Foo.vue'))

Directives

  • Now are internally on-vnode hooks with the exact same lifecycle as components.

  • Custom directives are now applied via a helper:

import { applyDirective, resolveDirective } from 'vue'

render (h) {
  // equivalent for v-my-dir
  const myDir = resolveDirective(this, 'my-dir')
  return applyDirective(h('div', 'hello'), [[myDir, this.someValue]])
}

Styles

No longer performs auto-prefixing.

Attributes

  • No longer auto coerces boolean or enumerated attribute values.
  • No longer removes attribute if value is boolean false. Instead, it's set as attr="false" instead. To remove the attribute, use null.

Filters

Filters are gone for good (or can it?)

Refs

  • Function refs are now supported.
  • String refs are no longer supported in render functions (now only supported in templates and compiled into function refs)
  • String refs no longer automatically generates an array when used with v-for. Instead, use something like :ref="'foo' + key" or function refs.

Compatibility build: How do we replace the component proxy's functionality?

So I was wondering about this ... and it's more a question of curiosity than me spotting a problem but ...

We will provide a compatibility version of the observer package that will implement the 2.0 Reactivity system (getters&setters) that people can use for browsers that don't support Proxy (read: IE).

However, also heavily rely on Proxy in the runtime-core (componentProxy.ts)

With this implementation, we don't have to actually add properties to the component instance for each prop and $data property, computed property and so on. We can just catch get and set access in the component's proxy.

But for a compatibility build, we would have to replace this behaviour with code that dpoes add getters & setters to the component instance, right?

In the current implementtion I don't see a clean / easy way to exchange this like we plan to have it for the observer package, do we (@evan) have a plan for how to approach it?

Add isTracked & onTrack api to check if a reactive object is being used in computed

What problem does this feature solve?

I'm using @vue/reactivity as a low level data service, and there are some heavy computing case in my app. I want to manually check if a reactive object is being used in computed values, so I can do lazy computing when it is not.

What does the proposed API look like?

export function isTracked(target) {
return targetMap.has(target)
}

export function onTrack(target, callback) {
// when a target is being tracked, call callback function.
}

Provide/Inject ("Context") API

For Reference: https://github.com/vuejs/vue-next/blob/master/packages/runtime-core/src/optional/context.ts

So I took a look at the new implementation and I have some reservations about it.

vuejs/vuex#1417

The current implementation in vue-next

As of this writing, vue-next will implement Context (or as we call it: provide/Inject) as a pair of components:

<!-- Parent.vue -->
<Provide id="someName" :value="{ message: 'Hello Vue!' }">
  <Child />
</Provide>
<!-- Child-vue -->
<Inject id="someName">
  <!-- side note: I couldn't determine if we will still require `slot-scope` 
       to be set on the root slot element, or if we provide a way to define it 
       on the component providing the slot itself.
  -->
  <div slot-scope="{ message }">
    {{ message }}
  <div>
</inject>

Reservations/Issues with this implementation

My reservations mostly originate from the discussions in the vuex repository about the proposed removal of mapXXX helpers in the next major of vuex here:

https://github.com/vuejs/vue-next/blob/master/packages/runtime-core/src/optional/context.ts

The important point is this one:

vuex state/getters/actions are only conveniently available in templates and render functions. That means the convenience is unavailable for computed properties that rely on Vuex state and methods that dispatch Vuex actions, which is a big limitation.

I think <Inject> suffers from similar problems. A component that uses <Inject> can only access the provided data within the template, so computed properties and watch callbacks have no way of accessing that data.

It works for React because React has neither of those things, it only has state, props and methods. So you can easily pass the injected data as an argument to a component method. But a similar thing is not possible for computed or watch functions since those don't take arguments.

Hooks

Maybe the new hot kid in town, Hooks, can solve this problem?

import { useInject } from 'vue'

export default {
  hooks() {
    const [injectedData] = useInject('someName')
    return {  
      injectedData 
    }
  }
}

That doesn't mean we have to drop <Inject>, I think it's still convenient for situations where accessing the injected Data in the template / render function is enough. But we need a way to allow users to actually inject this data as a property on the component instance.

The issue with a hook would be that we make this dependent on an experimental feature.

Backwards compatibility

We sold provide/inject as a feature primarily targeted at library authors. Dropping the old syntax from Vue 2 would be another breaking change that may be a roadblock for people updating their projects, as libs using this feature would have to find a way to make it work with the new component-based approach.

So this is another feature I think we should provide compat version for...

Attribute fallthrough behavior

Current proposal:

  • Remove inhertiAttrs option
  • Remove nativeOn
  • Remove automatic attribute fallthrough in all cases
  • The only, consistent way to enable attribute fallthrough is to explicitly spread $attrs on desired target element
    • In template: <div v-bind="$attrs">
    • In render functions: return cloneVNode(h('div'), this.$attrs)
    • note if component has no declared props, $attrs will be empty and everything will be in $props, and it should just spread / pass on $props instead. $attrs will just point to $props which includes everything passed to the component.

Rationale

  • The fallthrough behavior has already been inconsistent between stateful components and functional components in 2.x. With the introduction of fragments, the fallthrough behavior becomes even more unreliable for component consumers. The implicit behavior is convenient in cases where it works, but can be confusing in cases where it doesn't. By making this an explicit decision of component authors, whether a component accepts additional attributes becomes part of the component's API contract, overall resulting in more consistency.

  • The combination of inheritAttrs, nativeOn, $attrs and $listeners makes props passing in higher-order components unnecessarily complex. The new behavior makes it much more straightforward: spreading $attrs means "pass everything that I don't care about down to this element/component".

Drawbacks

  • Fallthrough behavior is now disabled by default and is controlled by the component author. If the component is intentionally "closed" there's no way for the consumer to change that. This may cause some inconvenience for users accustomed to the old behavior, but can be easily worked around with by wrapping the component in a wrapper element.

  • For accessibility reasons, it should be a best practice for components that are shipped as libraries to always spread $attrs. However this is a straightforward / mechanical code change, and is more of an educational issue. We could make it common knowledge by emphasizing this in all our information channels.

Side Benefit

Not spreading $attrs actually improves performance.

Custom Directive API Change

  • Start Date: 2019-04-09
  • Target Major Version: 3.x
  • Reference Issues: N/A
  • Implementation PR: N/A

Summary

  • Re-design custom directive API so that it better aligns with component lifecycle

  • Custom directives usage on components will follow the same rules as discussed in the Attribute Fallthrough Behavior RFC. It will be controlled by the child component via v-bind="$attrs".

Basic example

Before

const MyDirective = {
  bind(el, binding, vnode, prevVnode) {},
  inserted() {},
  update() {},
  componentUpdated() {},
  unbind() {}
}

After

const MyDirective = {
  beforeMount(el, binding, vnode, prevVnode) {},
  mounted() {},
  beforeUpdate() {},
  updated() {},
  beforeUnmount() {}, // new
  unmounted() {}
}

Motivation

Make custom directive hook names more consistent with the component lifecycle.

Detailed design

Hooks Renaming

Existing hooks are renamed to map better to the component lifecycle, with some timing adjustments. Arguments passed to the hooks remain unchanged.

  • bind -> beforeMount
  • inserted -> mounted
  • beforeUpdate new, called before the element itself is updated
  • update removed, use updated instead
  • componentUpdated -> updated
  • beforeUnmount new
  • unbind -> unmounted

Usage on Components

In 3.0, with fragments support, components can potentially have more than one root nodes. This creates an issue when a custom directive is used on a component with multiple root nodes.

To explain the details of how custom directives will work on components in 3.0, we need to first understand how custom directives are compiled in 3.0. For a directive like this:

<div v-foo="bar"></div>

Will roughly compile into this:

const vFoo = resolveDirective('foo')

return applyDirectives(h('div'), this, [
  [vFoo, bar]
])

Where vFoo will be the directive object written by the user, which contains hooks like mounted and updated.

applyDirective returns a cloned VNode with the user hooks wrapped and injected as vnode lifecycle hooks (see Render Function API Changes for more details):

{
  vnodeMounted(vnode, prevVNode) {
    // call vFoo.mounted(...)
  }
}

As a result, custom directives are fully included as part of a VNode's data. When a custom directive is used on a component, these vnodeXXX hooks are passed down to the component as extraneous props and end up in this.$attrs.

This is consistent with the attribute fallthrough behavior discussed in vuejs/rfcs#26. So, the rule for custom directives on a component will be the same as other extraneous attributes: it is up to the child component to decide where and whether to apply it. When the child component uses v-bind="$attrs" on an inner element, it will apply any custom directives used on it as well.

Drawbacks

N/A

Alternatives

N/A

Adoption strategy

  • The renaming should be easy to support in the compat build
  • Codemod should also be straightforward
  • For directives used on components, the warning on unused $attrs as discussed in Attribute Fallthrough Behavior should apply as well.

Unresolved questions

N/A

Render Function API Change

  • Start Date: 2019-03-12
  • Target Major Version: 3.x
  • Reference Issues: N/A
  • Implementation PR: N/A

Summary

  • h is now globally imported instead of passed to render functions as argument
  • render function arguments changed and made consistent between stateful and functional components
  • VNodes now have a flat data structure

Basic example

// globally imported `h`
import { h } from 'vue'

export default {
  // adjusted render function arguments
  render(props, slots) {
    return h(
      'div',
      // flat data structure
      { id: props.id },
      slots.default()
    )
  }
}

Motivation

In 2.x, VNodes are context-specific - which means every VNode created is bound to the component instance that created it (the "context"). This is because we need to support the following use cases:

// looking up a component based on a string ID
h('some-component')

h('div', {
  directives: [
    {
      name: 'foo', // looking up a directive by string ID
      // ...
    }
  ]
})

In order to look up locally/globally registered components and directives, we need to know the context component instance that "owns" the VNode. This is why in 2.x h is passed in as an argument, because the h passed into each render function is a curried version that is pre-bound to the context instance.

This has created a number of inconveniences, for example when trying to extract part of the render logic into a separate function, h needs to be passed along:

function renderSomething(h) {
  return h('div')
}

export default {
  render(h) {
    return renderSomething(h)
  }
}

When using JSX, this is especially cumbersome since h is used implicitly and isn't needed in user code. Our JSX plugin has to perform automatic h injection in order to alleviate this, but the logic is complex and fragile.

In 3.0 we have found ways to make VNodes context-free. They can now be created anywhere using the globally imported h function, so it only needs to be imported once in any file.


Another issue with 2.x's render function API is the nested VNode data structure:

h('div', {
  class: ['foo', 'bar'],
  style: { }
  attrs: { id: 'foo' },
  domProps: { innerHTML: '' },
  on: { click: foo }
})

This structure was inherited from Snabbdom, the original virtual dom implementation Vue 2.x was based on. The reason for this design was so that the diffing logic can be modular: an individual module (e.g. the class module) would only need to work on the class property. It is also more explicit what each binding will be processed as.

However, over time we have noticed there are a number of drawbacks of the nested structure compared to a flat structure:

  • More verbose to write
  • class and style special cases are somewhat inconsistent
  • More memory usage (more objects allocated)
  • Slower to diff (each nested object needs its own iteration loop)
  • More complex / expensive to clone / merge / spread
  • Needs more special rules / implicit conversions when working with JSX

In 3.x, we are moving towards a flat VNode data structure to address these problems.

Detailed design

Globally imported h function

h is now globally imported:

import { h } from 'vue'

export default {
  render() {
    return h('div')
  }
}

Render Function Arguments Change

With h no longer needed as an argument, the render function now receives a new set of arguments:

// MyComponent.js
export default {
  render(
    // declared props
    props,
    // resolved slots
    slots,
    // fallthrough attributes
    attrs,
    // the raw vnode in parent scope representing this component
    vnode
  ) {

  }
}
  • props and attrs will be equivalent to this.$props and this.$attrs - also see Optional Props Declaration and Attribute Fallthrough

  • slots will be equivalent to this.$slots - also see Slots Unification

  • vnode will be equivalent to this.$vnode, which is the raw vnode that represents this component in parent scope, i.e. the return value of h(MyComponent, { ... }).

Note that the render function for a functional component will now also have the same signature, which makes it consistent in both stateful and functional components:

const FunctionalComp = (props, slots, attrs, vnode) => {
  // ...
}

The new list of arguments should provide the ability to fully replace the current functional render context:

  • props and slots have equivalent values

  • data and children can be accessed directly on vnode

  • listeners will be included in attrs

  • injections will have a dedicated new API:

    import { resolveInjection } from 'vue'
    import { themeSymbol } from './ThemeProvider'
    
    const FunctionalComp = props => {
      const theme = resolveInjection(themeSymbol)
      return h('div', `Using theme ${theme}`)
    }
  • parent access will be removed. This was an escape hatch for some internal use cases - in userland code, props and injections should be preferred.

Flat VNode Data Format

// before
{
  class: ['foo', 'bar'],
  style: { color: 'red' },
  attrs: { id: 'foo' },
  domProps: { innerHTML: '' },
  on: { click: foo },
  key: 'foo'
}

// after
{
  class: ['foo', 'bar'],
  style: { color: 'red' },
  id: 'foo',
  innerHTML: '',
  onClick: foo,
  key: 'foo'
}

With the flat structure, the VNode data props are handled using the following rules:

  • key, ref and slots are reserved special properties
  • class and style have the same API as 2.x
  • props that start with on are handled as v-on bindings
  • for anything else:
    • If the key exists as a property on the DOM node, it is set as a DOM property;
    • Otherwise it is set as an attribute.

Due to the flat structure, this.$attrs inside a component now also contains any raw props that are not explicitly declared by the component, including onXXX listeners. This makes it much easier to write wrapper components - simply pass this.$attrs down with v-bind="$attrs" (as a result, this.$listeners will also be removed).

Context-free VNodes

With VNodes being context-free, we can no longer use a string ID (e.g. h('some-component')) to implicitly lookup globally registered components. Same for looking up directives. Instead, we need to use an imported API:

import { h, resolveComponent, resolveDirective, applyDirectives } from 'vue'

export default {
  render() {
    const comp = resolveComponent('some-global-comp')
    const fooDir = resolveDirective('foo')
    const barDir = resolveDirective('bar')

    // <some-global-comp v-foo="x" v-bar="y" />
    return applyDirectives(
      h(comp),
      this,
      [fooDir, this.x],
      [barDir, this.y]
    )
  }
}

This will mostly be used in compiler-generated output, since manually written render function code typically directly import the components and use them by value, and use rarely have to use directives.

Drawbacks

Reliance on Vue Core

h being globally imported means any library that contains Vue components will include import { h } from 'vue' somewhere (this is implicitly included in render functions compiled from templates as well). This creates a bit of overhead since it requires library authors to properly configure the externalization of Vue in their build setup:

  • Vue should not be bundled into the library;
  • For module builds, the import should be left alone and be handled by the end user bundler;
  • For UMD / browser builds, it should try the global Vue.h first and fallback to require calls.

This is common practice for React libs and possible with both webpack and Rollup. A decent number of Vue libs also already does this. We just need to provide proper documentation and tooling support.

Alternatives

N/A

Adoption strategy

  • For template users this will not affect them at all.

  • For JSX users the impact will also be minimal, but we do need to rewrite our JSX plugin.

  • Users who manually write render functions using h will be subject to major migration cost. This should be a very small percentage of our user base, but we do need to provide a decent migration path.

    • It's possible to provide a compat plugin that patches render functions and make them expose a 2.x compatible arguments, and can be turned off in each component for a one-at-a-time migration process.

    • It's also possible to provide a codemod that auto-converts h calls to use the new VNode data format, since the mapping is pretty mechanical.

  • Functional components using context will likely have to be manually migrated, but a smilar adaptor can be provided.

Can not use reactive as watchsource

Version

3.0.0-alpha.1

Reproduction link

https://jsfiddle.net/v27hqo1a/

Steps to reproduce

Not sure if this is intended but this code won't work:

watch(reactive({"test": "test"}), () => console.log("foo"))

but for normal refs, this works perfectly fine:

watch(ref(0), () => console.log("foo"))

will reactives also be supported?

What is expected?

reactive object should be watched

What is actually happening?

it will not be watched


to see the error within the fiddle just open the console.

Attributes gets converted to camelCase without explicit props declaration

Version

3.0.0-alpha.1

Reproduction link

https://codesandbox.io/s/zen-field-ewni3

vue 2 for comparison - https://jsfiddle.net/jfrhgbma/

Steps to reproduce

import { h, createApp } from "vue";

function Button(props, { attrs }) {
  console.log({
    props,
    attrs
  });
}

const App = {
  setup() {
    return () => [
      h(Button, {
        "data-id": 1,
        "aria-label": "Close",
      })
    ];
  }
};

createApp().mount(App, "#root");

What is expected?

Button's props should be in camelCase and attrs in kebab-case

What is actually happening?

Without explicit props declaration Button attrs keys are converted to camelCase.

[SSR] Async renderToString to allow data-prefetching

This would allow for automatic data prefetching (for example with apollo) on the server.

Ideas:

  • waitCounter = 0
  • mount the App and render the whole component tree
  • user/lib code can hook into renderToString with this.waitForPrefetch() (waitCounter++)
  • if waitCounter is 0, resolve renderToString
  • else, we wait for user/lib code that should call this.prefetchDone() after fetching data
    • we re-render the component
    • eventually code in children can call this.waitForPrefetch() again
    • then waitCounter--
    • then if waitCounter is 0 we resolve renderToString
  • a re-render could trigger new this.waitForPrefetch() (maybe make this parametrable, like maxReRenders)
  • <NoSSR> component prevents both server-side render and async prefetching
  • some way of skipping a component (and its children) only for async prefetching, like a skipPrefetch () { return this.myProp } option?

[SSR] Fake instances tree for smart prefetching

I think it would be nice to have an official way of "pre-rendering" the components tree to gather prefetching data when doing SSR. Ideally much lighter than a real render, with ways to mock global/local properties and methods to speed it up, maybe with an API libraries can use to mock themselves.

This would hugely improve the SSR story because currently we are limited to the routes components. I recently made some experimentation with this in the vue-apollo SSR API that allows the user to prefetch all the GraphQL queries in his app without manually adding them if they are in rotue sub-components (or even outside of router views). However, I think it would be better as an official API so it's less exposed to breaking due to Vue internal changes.

The vue-apollo implementation also recognizes a special attribute like no-prefetch which skips a components sub-tree to optimize the tree walking if it is known that no queries will be found there.

[Abandoned] API for React Hooks like logic composition

Note: this has been split into two separate RFCs: #24 & #25


  • Start Date: 03-05-2019
  • Target Major Version: 2.x & 3.x
  • Reference Issues: N/A
  • Implementation PR: N/A

Summary

A set of React-hooks-inspired APIs for composing and reusing stateful logic across components.

Basic example

In Object API

import { value, computed, watch, useMounted, useDestroyed } from 'vue'

export default {
  created() {
    // value returns a value "ref" that has a .value property
    const count = value(0)

    // computed returns a computed "ref" with a read-only .value property
    const plusOne = computed(() => count.value + 1)

    // refs can be watched directly (or explicitly with `() => count.value`)
    watch(count, (count, oldCount) => {
      console.log(`count changed to: ${count}`)
    })

    watch(plusOne, countPlusOne => {
      console.log(`count plus one changed to: ${countPlusOne}`)
    })

    useMounted(() => {
      console.log('mounted')
    })

    useDestroyed(() => {
      console.log('destroyed')
    })

    // bind refs as properties to the instance. This exposes
    // this.count as a writable data property and this.plusOne as a read-only
    // computed property
    return {
      count,
      plusOne
    }
  }
}

In Class API

Usage in a Class-based API would be exactly the same:

import Vue, { useMounted } from 'vue'

export default class MyComponent extends Vue {
  created() {
    useMounted(() => {
      console.log('mounted')
    })
  }
}

Motivation

  • React hooks like composition, but fits Vue's idiomatic usage
  • A more composable replacement for Mixins

Detailed design

Summary

This proposal consists of three parts:

  1. A set of APIs centered around reactivity, e.g. value, computed and watch. These will be part of the @vue/observer package, and re-exported in the main vue package. These APIs can be used anywhere and isn't particularly bound to the usage outlined in this proposal, however they are quintessential in making this proposal work.

  2. A set of call-site constrained APIs that registers additional lifecycle hooks for the "current component", e.g. useMounted. These functions can only be called inside the created() lifecycle hook of a component.

  3. The ability for the created() lifecycle hook to return an object of additional properties to expose on the component instance.

Reactivity APIs

In Vue 2.x, we already have the observable API for creating standalone reactive objects. Assuming we can return an object of additional properties to expose on this from created(), we can achieve the following:

import { observable } from 'vue'

const App = {
  created() {
    const state = observable({
      count: 0
    })

    const increment = () => {
      state.count++
    }

    // exposed on `this`
    return {
      state,
      increment
    }
  },
  template: `
    <button @click="increment">{{ state.count }}</button>
  `
}

The above is a contrived example just to demonstrate how it could work. In practice, this is intended mainly for encapsulating and reusing logic much more complex than a simple counter.

Value Container

Note in the above example we had to expose an object (so that Vue can register the dependency via the property access during render) even though what we are really exposing is just a number. We can use the value API to create a container object for a single value, called a "ref". A ref is simply a reactive object with a writable value property that holds the actual value:

import { value } from 'vue'

const countRef = value(0)

// read the value
console.log(countRef.value) // 0

// mutate the value
countRef.value++

The reason for using a container object is so that our code can have a persistent reference to a value that may be mutated over time.

A value ref is very similar to a plain reactive object with only the .value property. It is primarily used for holding primitive values, but the value can also be a deeply nested object, array or anything that Vue can observe. Deep access are tracked just like typical reactive objects. The main difference is that when a ref is returned as part of the return object in created(), it is bound as a direct property on the component instance:

import { value } from 'vue'

const App = {
  created() {
    return {
      count: value(0)
    }
  },
  template: `
    <button @click="count++">{{ count }}</button>
  `,
}

A ref binding exposes the value directly, so the template can reference it directly as count. It is also writable - note that the click handler can directly do count++. (In comparison, non-ref bindings returned from created() are readonly).

Computed State

In addition to writable value refs, we can also create standalone computed refs:

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

Computed refs are readonly. Assigning to its value property will result in an error.

Watchers

All .value access are reactive, and can be tracked with the standalone watch API.

import { value, computed, watch } from 'vue'

const count = value(0)
const double = computed(() => count.value * 2)

// watch and re-run the effect
watch(() => {
  console.log('count is: ', count.value)
})
// -> count is: 0

// 1st argument (the getter) can return a value, and the 2nd
// argument (the callback) only fires when returned value changes
watch(() => count.value + 1, value => {
  console.log('count + 1 is: ', value)
})
// -> count + 1 is: 1

// can also watch a ref directly
watch(double, value => {
  console.log('double the count is: ', value)
})
// -> double the count is: 0

count.value++
// -> count is: 1
// -> count + 1 is: 2
// -> double the count is: 2

Note that unlike this.$watch in 2.x, watch are immediate by default (defaults to { immediate: true }) unless a 3rd options argument with { lazy: true } is passed:

watch(
  () => count.value + 1,
  () => {
    console.log(`count changed`)
  },
  { lazy: true }
)

The callback can also return a cleanup function which gets called every time when the watcher is about to re-run, or when the watcher is stopped:

watch(someRef, value => {
  const token = performAsyncOperation(value)
  return () => {
    token.cancel()
  }
})

A watch returns a stop handle:

const stop = watch(...)

// stop watching
stop()

Watchers created inside a component's created() hook are automatically stopped when the owner component is destroyed.

Lifecycle Hooks

For each existing lifecycle hook (except beforeCreate and created), there will be an equivalent useXXX API. These APIs can only be called inside the created() hook of a component. The prefix use is an indication of the call-site constraint.

import { useMounted, useUpdated, useDestroyed } from 'vue'

export default {
  created() {
    useMounted(() => {
      console.log('mounted')
    })

    useUpdated(() => {
      console.log('updated')
    })

    useDestroyed(() => {
      console.log('destroyed')
    })
  }
}

Unlike React Hooks, created is called only once, so these calls are not subject to call order and can be conditional.

useXXX methods automatically detects the current component whose setup() is being called. The instance is also passed into the registered lifecycle hook as the argument. This means they can easily be extracted and reused across multiple components:

import { useMounted } from 'vue'

const useSharedLogic = () => {
  useMounted(vm => {
    console.log(`hello from component ${vm.$options.name}`)
  })
}

const CompA = {
  name: 'CompA',
  created() {
    useSharedLogic()
  }
}

const CompB = {
  name: 'CompB',
  created() {
    useSharedLogic()
  }
}

A More Practical Example

Let's take the example from React Hooks Documentation. Here's the equivalent in Vue's idiomatic API:

export default {
  props: ['id'],
  data() {
    return {
      isOnline: null
    }
  },
  created() {
    ChatAPI.subscribeToFriendStatus(this.id, this.handleStatusChange)
  },
  destroyed() {
    ChatAPI.unsubscribeFromFriendStatus(this.id, this.handleStatusChange)
  },
  watch: {
    id: (newId, oldId) => {
      ChatAPI.unsubscribeFromFriendStatus(oldId, this.handleStatusChange)
      ChatAPI.subscribeToFriendStatus(newId, this.handleStatusChange)
    }
  },
  methods: {
    handleStatusChange(status) {
      this.isOnline = status
    }
  }
}

And here's the equivalent using the APIs introduced in this proposal:

import { value, computed, watch } from 'vue'

export default {
  props: ['id'],
  created() {
    const isOnline = value(null)

    function handleStatusChange(status) {
      isOnline.value = status
    }

    watch(() => this.id, id => {
      // this is called immediately and then very time id changes
      ChatAPI.subscribeToFriendStatus(id, handleStatusChange)
      return () => {
        // this is called every time id changes and when the component
        // is unmounted (which causes the watcher to be stopped)
        ChatAPI.unsubscribeFromFriendStatus(id, handleStatusChange)
      }
    })

    return {
      isOnline
    }
  }
}

Note that because the watch function is immediate by default and also auto-stopped on component unmount, it achieves the effect of watch, created and destroyed options in one call (similar to useEffect in React Hooks).

The logic can also be extracted into a reusable function (like a custom hook):

import { value, computed, watch } from 'vue'

function useFriendStatus(idRef) {
  const isOnline = value(null)

  function handleStatusChange(status) {
    isOnline.value = status
  }

  watch(idRef, id => {
    ChatAPI.subscribeToFriendStatus(id, handleStatusChange)
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(id, handleStatusChange)
    }
  })

  return isOnline
}

export default {
  props: ['id'],
  created() {
    return {
      // to pass watch-able state, make sure to pass a value
      // or computed ref
      isOnline: useFriendStatus(computed(() => this.id))
    }
  }
}

Note that even after logic extraction, the component is still responsible for:

  • Declaring all the props it expects
  • Declaring all the properties to expose to the template

Even with multiple extracted custom logic functions, there will be no confusion regarding where a prop or a data property comes from. This is a major advantage over mixins.

Drawbacks

  • Type inference of returned values. This actually works better in the object-based API because we can reverse infer this based on the return value of created(), but it's harder to do so in a class. The user will likely have to explicitly annotate these properties on the class.

  • To pass state around while keeping them "trackable" and "reactive", values must be passed around in the form of ref containers. This is a new concept and can be a bit more difficult to learn than the base API. However, this isn't intended as a replacement for the base API - it is positioned as a advanced mechanism to encapsulate and reuse logic across components.

Alternatives

Compared to React hooks:

  • same composition capability
  • closer mapping to Vue's existing usage
  • no repeated invocation, less wasted memory on repeated render
  • watch has automatic dependency tracking, no need to worry about exhaustive deps
  • reactive state are always referenced via refs so no confusion of stale closures
  • does not rely on call order

Adoption strategy

TODO

Unresolved questions

  • We can also use data() instead of created(), since data() is already used for exposing properties to the template. But it feels a bit weird to perform side effects like watch or useMounted in data().

    • Maybe we can introduce a new option dedicated for this purpose, e.g. state()? (replaces data() and has special handling for refs in returned value)
  • We probably need to also expose a isRef method to check whether an object is a value/computed ref.

Appendix: More Usage Examples

Data Fetching

This is the equivalent of what we can currently achieve via scoped slots with libs like vue-promised. This is just an example showing that this new set of API is capable of achieving similar results.

function useFetch(endpointRef) {
  const res = value({
    status: 'pending',
    data: null,
    error: null
  })

  // watch can directly take a computed ref
  watch(endpointRef, endpoint => {
    let aborted = false
    fetch(endpoint)
      .then(res => res.json())
      .then(data => {
        if (aborted) {
          return
        }
        res.value = {
          status: 'success',
          data,
          error: null
        }
      }).catch(error => {
        if (aborted) {
          return
        }
        res.value = {
          status: 'error',
          data: null,
          error
        }
      })
    return () => {
      aborted = true
    }
  })

  return res
}

// usage
const App = {
  created(props) {
    return {
      postData: useFetch(computed(() => `/api/posts/${props.id}`))
    }
  },
  template: `
    <div>
      <div v-if="postData.status === 'pending'">
        Loading...
      </div>
      <div v-else-if="postData.status === 'success'">
        {{ postData.data }}
      </div>
      <div v-else>
        {{ postData.error }}
      </div>
    </div>
  `
}

Use the Platform

Hypothetical examples of exposing state to the component from external sources. This is more explicit than using mixins regarding where the state comes from.

export default {
  created() {
    const { x, y } = useMousePosition()
    const orientation = useDeviceOrientation()
    return {
      x,
      y,
      orientation
    }
  }
}

[Discuss] Setup function arguments

Version

3.0.0-alpha.1

Reproduction link

https://have.not

Steps to reproduce

I accidentally noticed that if I write setup function like this, I will not receive the context parameter.

export default {
  setup(...args) {
    console.log(args[1]) // null
  }
}

What is expected?

args[1] is context

What is actually happening?

args[1] is null


I found why:
https://github.com/vuejs/vue-next/blob/master/packages/runtime-core/src/component.ts#L293

I know most people won't write like this, but it's a little weird for me.

Just for discuss.

Hooks integration

Rationale

https://twitter.com/youyuxi/status/1056673771376050176

Hooks provides the ability to:

  • encapsulate arbitrarily complex logic in plain functions
  • does not pollute component namespace (explicit injection)
  • does not result in additional component instances like HOCs / scoped-slot components
  • superior composability, e.g. passing the state from one hook to another effect hook. This is possible by referencing fields injected by other mixins in a mixin, but that is super flaky and hooks composition is way cleaner.
  • compresses extremely well

However, it is quite different from the intuitions of idiomatic JS, and has a number of issues that can be confusing to beginners. This is why we should integrate it in a way that complements Vue's existing API, and primarily use it as a composition mechanism (replacement of mixins, HOCs and scoped-slot components).

Proposed usage

Directly usable inside class render functions (can be mixed with normal class usage):

class Counter extends Component {
  foo = 'hello'
  render() {
    const [count, setCount] = useState(0)
    return h(
      'div',
      {
        onClick: () => {
          setCount(count + 1)
        }
      },
      this.foo + ' ' + count
    )
  }
}

For template usage:

class Counter extends Component {
  static template = `
    <div @click="setCount(count + 1)">
      {{ count }}
    </div>
  `

  hooks() {
    const [count, setCount] = useState(0)
    // fields returned here will become available in templates
    return {
      count,
      setCount
    }
  }
}

In SFC w/ object syntax:

<template>
  <div @click="setCount(count + 1)">
    {{ count }}
  </div>
</template>

<script>
import { useState } from 'vue'

export default {
  hooks() {
    const [count, setCount] = useState(0)
    return {
      count,
      setCount
    }
  }
}
</script>

Note: counter is a super contrived example mainly to illustrate how the API works. A more practical example would be this useAPI custom hook, which is similar to libs like vue-promised.

Implementation Notes

Proposed usage for useState and useEffect are already implemented.

Update: Mapping w/ Vue's existing API

To ease the learning curve for Vue users, we can implement hooks that mimic Vue's current API:

export default {
  render() {
    const data = useData({
      count: 0
    })

    useWatch(() => data.count, (val, prevVal) => {
      console.log(`count is: ${val}`)
    })

    const double = useComputed(() => data.count * 2)

    useMounted(() => {
      console.log('mounted!')
    })
    useUnmounted(() => {
      console.log('unmounted!')
    })
    useUpdated(() => {
      console.log('updated!')
    })

    return [
      h('div', `count is ${data.count}`),
      h('div', `double count is ${double}`),
      h('button', { onClick: () => {
        // still got that direct mutation!
        data.count++
      }}, 'count++')
    ]
  }
}

Basic store pattern example doesn't work

Version

3.0.0-alpha.1

Reproduction link

https://jsfiddle.net/fva48tsr/

Steps to reproduce

I tried out the basic store pattern example from the docs, but it doesn't ever update.

<template>
  <div>{{ sharedState.counter }}</div>
</template>

<script>
var store = {
  state: {
    counter: 0
  },
  increment: function() {
    this.state.counter  ;
  }
};

export default {
  data: function() {
    return {
      sharedState: store.state
    };
  },
  mounted: function() {
    setInterval(() => {
      store.increment();
    }, 1000);
  }
}
</script>

What is expected?

The counter goes up

What is actually happening?

The counter doesn't change

About "block-based diff algorithm"

@yyx990803
Hi Evan, the new diff algorithm is really amazing, thank you for your efforts, but I have a question about this, assuming the following code is generated by the compiler:

(
  openBlock(),
  createBlock('div', null, [        // block_one
    h('p', 123),
    h('p', this.name),
    this.isTruth
      ? (
          openBlock(),
          createBlock('div', null, [  // block_two
            h('p', 456),
            h('p', this.text1) 
          ])
        )
      : (
          openBlock(),
          createBlock('div', null, [  // block_three
            h('p', this.text2) 
          ])
        )
  ])
)

When isTruth is true then:

block_one.dynamicChildren = [ // oldBlockTree
  h('p', this.name),
  block_two
]

When isTruth becomes false:

block_one.dynamicChildren = [ // newBlockTree
  h('p', this.name),
  block_three
]

When diff oldBlockTree and newBlockTree, since block_two and block_three are different blocks, their children may have different structures. If we only diff their dynamic children, it should be incorrect. I just checked the code here https://github.com/vuejs/vue-next/blob/master/packages/runtime-core/src/createRenderer.ts#L367-L380 What important information did I miss? I am not sure if the above example is reasonable, If not, then simply close the issue.

Simplifying the transparent wrapper pattern

The transparent wrapper pattern is very convenient and increasingly common, but still a little clunky in Vue 2.x. For example, to get a BaseInput component that can be used exactly like a normal input element, we need to do this:

<script>
export default {
  props: {
    value: {
      type: String
    }
  },
  computed: {
    listeners() {
      return {
        ...this.$listeners,
        input: event => {
          this.$emit('input', event.target.value)
        }
      }
    }
  }
}
</script>

<template>
  <input
    :value="value"
    v-on="listeners"
  >
</template>

In Vue 3, that will currently translate to:

<script>
class InputText extends Component {
  // Currently, `v-model` on a component assumes `value` is a 
  // prop, so it will never be included in $attrs, even if no
  // `value` prop is defined. This forces users to always define 
  // `value` as a prop, even if it's never needed by the component.
  static props = {
    value: String
  }

  get attrs () {
    return {
      ...this.$attrs,
      // We have to manually include the `value` prop to `attrs`,
      // due to `v-model`'s assumption that `value` is a prop.
      value: this.value,
      // Since `v-model` listens to the `input` event by default, 
      // the user is forced to override it when using binding 
      // $attrs in order to prevent the unwanted behavior of the
      // `event` object being emitted as the new value.
      onInput: event => {
        this.$emit('input', event.target.value)
      }
    }
  }
}
</script>

<template>
  <input v-bind="attrs">
</template>

Still quite a bit of work. ๐Ÿ˜•

However, if we remove v-model's assumption that value is a prop, then it no longer has to be added separately. And if we also make v-model listen for an update:value event by default (as @posva suggested here), instead of input, then we no longer have to override input and can just add a new listener.

With these 2 changes, our BaseInput component would simplify to:

<template>
  <input
    v-bind="$attrs"
    @input="$emit('update:value', $event.target.value)"
  >
</template>

To me, this is much more convenient and intuitive. ๐Ÿ™‚

@yyx990803 @posva @johnleider What do you think?

RFC Draft: Function-based Component API

  • Start Date: 2019-05-30
  • Target Major Version: 2.x / 3.x
  • Reference Issues: vuejs/rfcs#22 vuejs/rfcs#23
  • Implementation PR: (leave this empty)

Summary

Expose logic-related component options via function-based APIs instead.

Basic example

import { value, computed, watch, onMounted } from 'vue'

const App = {
  template: `
    <div>
      <span>count is {{ count }}</span>
      <span>plusOne is {{ plusOne }}</span>
      <button @click="increment">count++</button>
    </div>
  `,
  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 - 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, 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:

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: `<div>{{ x }} {{ y }} {{ z }}</div>`
}

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:

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, but after discussion and prototyping we discovered that using Classes doesn't fully address the typing issue.

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:

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:

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).

State

Similar to data(), setup() can return an object containing properties to be exposed to the template's render context:

const MyComponent = {
  props: {
    name: String
  },
  setup(props) {
    return {
      msg: `hello ${props.name}!`
    }
  },
  template: `<div>{{ msg }}</div>`
}

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:

import { value } from 'vue'

const MyComponent = {
  setup(props) {
    const msg = value('hello')
    const appendName = () => {
      msg.value = `hello ${props.name}`
    }
    return {
      msg,
      appendName
    }
  },
  template: `<div @click="appendName">{{ msg }}</div>`
}

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:

// 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:

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:

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 observable (which is an exact equivalent of 2.x Vue.observable API):

import { observable } from 'vue'

const object = observable({
  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:

const MyComponent = {
  setup() {
    return {
      count: value(0)
    }
  },
  template: `<button @click="count++">{{ count }}</button>`
}

Value wrappers are also automatically unwrapped when accessed as a nested property inside a reactive object:

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:

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:

const count = value(0)
const writableComputed = computed(
  // read
  () => count.value + 1,
  // write
  val => {
    count.value = val - 1
  }
)

Watchers

All .value access are reactive, and can be tracked with the standalone watch API, which behaves like the 2.x vm.$watch API but with important differences.

The first argument passed to watch can be either a getter function or a value wrapper. The second argument is a callback that will only get called when the value returned from the getter or the value wrapper has changed:

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.

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:

const MyComponent = {
  props: {
    id: number
  },
  setup(props) {
    const data = value(null)
    watch(() => props.id, async (id) => {
      data.value = await fetchData(id)
    })
  }
}

Watching Value Wrappers

// 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

Stopping a Watcher

A watch call returns a stop handle:

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:

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())
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:

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:

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

interface WatchOptions {
  lazy?: boolean
  deep?: boolean
  flush?: 'pre' | 'post' | 'sync'
  onTrack?: (e: DebuggerEvent) => void
  onTrigger?: (e: DebuggerEvent) => void
}

Lifecycle Hooks

All current lifecycle hooks will have an equivalent onXXX function that can be used inside setup():

import { onMounted, onUpdated, onUnmounted } from 'vue'

const MyComponent = {
  setup() {
    onMounted(() => {
      console.log('mounted!')
    })
    onUpdated(() => {
      console.log('updated!')
    })
    onUnmounted(() => {
      console.log('unmounted!')
    })
  }
}

Dependency Injection

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. (Maybe that's a good thing?)

Alternatives

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.

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 value and value.raw returned from 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.

  • propsData

    Props for root component can be passed via app instance's mount method. See RFC#29.

  • functional

    Functional components are now declared as plain functions. See RFC#27.

  • model

    No longer necessary with v-model arguments. See RFC#31.

  • inheritAttrs

    Deperecated by RFC#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:

interface Props {
  message: string
}

class App extends Component<Props> {
  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:

class App extends Component<Props> {
  @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.

Different builds in Vue 3

Vue 2

So right now, in Vue 2, we have 14 different builds

Why? Because we have 4 different dimensions that define a build:

  1. Compiler?
  • Runtime+compiler
  • Runtime only
  1. Format
  • Commonjs
  • ESM
  • ESM.browser
  • UMD
  1. Minification (~ "mode")
  • Minified (= prod)
  • non-minified (= dev)

Vue 3

In Vue 3, we get 2 additional dimensions to the existing ones through compatibility builds:

  1. API comptibility
  • Function-based API only
  • ^+ compatibilty for Vue 2 object notation
  1. IE compatibility
  • Proxy-based reactivity
  • getter/setter based reactivity (a la Vue 2)

The issue

If we want to keep all permutations supported, we end up with:

14 * 4 = 56 different builds!

That's a lot of builds ๐Ÿ‘€ - and certainly too many to leave for the user to pick from.

Alternatives / Mitigation

Drop some dimension variations

  1. no commonjs for non-compatiblity builds? New APIs = new build systems = ES modules?
  2. ...?

compose at the package level?

import { createVue } from '@vue/core'
import apifrom '@vue/api-compat'
import ractivity from '@vue/reactivity-compat'
export default createVue({
  reactivity,
  api,
})

Seems cumbersome, especially since this combo will be the default starting point for migrating existing projects.

So... Toughts?

Global API Change

  • Start Date: (fill me in with today's date, YYYY-MM-DD)
  • Target Major Version: (2.x / 3.x)
  • Reference Issues: (fill in existing related issues, if any)
  • Implementation PR: (leave this empty)

Summary

Re-design app bootstrapping and global configuration API.

Basic example

Before

import Vue from 'vue'
import App from './App.vue'

Vue.config.ignoredElements = [/^app-/]
Vue.use(/* ... */)
Vue.mixin(/* ... */)
Vue.component(/* ... */)
Vue.directive(/* ... */)

new Vue({
  render: h => h(App)
}).$mount('#app')

After

import { createApp } from 'vue'
import App from './App.vue'

const app = createApp()

app.config.ignoredElements = [/^app-/]
app.use(/* ... */)
app.mixin(/* ... */)
app.component(/* ... */)
app.directive(/* ... */)

app.mount(App, '#app')

Motivation

Vue's current global API and configurations permanently mutate global state. This leads to a few problems:

  • Global configuration makes it easy to accidentally pollute other test cases during testing. Users need to carefully store original global configuration and restore it after each test (e.g. resetting Vue.config.errorHandler). Some APIs (e.g. Vue.use, Vue.mixin) don't even have a way to revert their effects. This makes tests involving plugins particularly tricky.

    • vue-test-utils has to implement a special API createLocalVue to deal with this
  • This also makes it difficult to share the same copy of Vue between multiple "apps" on the same page, but with different global configurations:

    // this affects both root instances
    Vue.mixin({ /* ... */ })
    
    const app1 = new Vue({ el: '#app-1' })
    const app2 = new Vue({ el: '#app-2' })

Detailed design

Technically, Vue 2 doesn't have the concept of an "app". What we define as an app is simply a root Vue instance created via new Vue(). Every root instance created from the same Vue constructor shares the same global configuration.

In this proposal we introduce a new global API, createApp:

import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)

Calling createApp with a root component returns an app instance. An app instance provides an app context. The entire component tree formed by the root instance and its descendent components share the same app context, which provides the configurations that were previously "global" in Vue 2.x.

Global API Mapping

An app instance exposes a subset of the current global APIs. The rule of thumb is any APIs that globally mutate Vue's behavior are now moved to the app instance. These include:

  • Global configuration
    • Vue.config -> app.config
      • with the exception of Vue.config.productionTip
  • Asset registration APIs
    • Vue.component -> app.component
    • Vue.directive -> app.directive
    • Vue.filter -> app.filter
  • Behavior Extension APIs
    • Vue.mixin -> app.mixin
    • Vue.use -> app.use

Global APIs that are idempotent (i.e. do not globally mutate behavior) are now named exports as proposed in Global API Treeshaking.

Mounting App Instance

The app instance can be mounted with the mount method. It works the same as the existing vm.$mount() component instance method and returns the mounted root component instance:

const rootInstance = app.mount('#app')

rootInstance instanceof Vue // true

Provide / Inject

An app instance can also provide dependencies that can be injected by any component inside the app:

// in the entry
app.provide({
  [ThemeSymbol]: theme
})

// in a child component
export default {
  inject: {
    theme: {
      from: ThemeSymbol
    }
  },
  template: `<div :style="{ color: theme.textColor }" />`
}

This is similar to using the provide option in a 2.x root instance.

Drawbacks

  • Global APIs are now split between app instance methods and global named imports, instead of a single namespace. However the split makes sense because:

    • App instance methods are configuration APIs that globally mutate an app's behavior. They are also almost always used together only in the entry file of a project.

    • Global named imports are idempotent helper methods that are typically imported and used across the entire codebase.

Alternatives

N/A

Adoption strategy

  • The transformation is straightforward (as seen in the basic example).
  • A codemod can also be provided.

Unresolved questions

  • Vue.config.productionTip is left out because it is indeed "global". Maybe it should be moved to a global method?

    import { suppressProductionTip } from 'vue'
    
    suppressProductionTip()

Dynamic Lifecycle Injection

  • Start Date: 03-05-2019
  • Target Major Version: 2.x & 3.x
  • Reference Issues: N/A
  • Implementation PR: N/A

Summary

Introduce APIs for dynamically injecting component lifecycle hooks.

Basic example

import { onMounted, onUnmounted } from 'vue'

export default {
  beforeCreate() {
    onMounted(() => {
      console.log('mounted')
    })

    onUnmounted(() => {
      console.log('unmounted')
    })
  }
}

Motivation

In advanced use cases, we sometimes need to dynamically hook into a component's lifecycle events after the component instance has been created. In Vue 2.x there is an undocumented API via custom events:

export default {
  created() {
    this.$on('hook:mounted', () => {
      console.log('mounted!')
    })
  }
}

This API has some drawbacks because it relies on the event emitter API with string event names and a reference of the target component instance:

  1. Event emitter APIs with string event names are prone to typos and is hard to notice when a typo is made because it fails silently.

  2. If we were to extract complex logic into external functions, the target instance has to be passed to it via an argument. This can get cumbersome when there are additional arguments, and when trying to further split the function into smaller functions. When called inside a component's data() or lifecycle hooks, the target instance can already be inferred by the framework, so ideally the instance reference should be made optional.

This proposal addresses both problems.

Detailed design

For each existing lifecycle hook (except beforeCreate), there will be an equivalent onXXX API:

import { onMounted, onUpdated, onDestroyed } from 'vue'

export default {
  created() {
    onMounted(() => {
      console.log('mounted')
    })

    onUpdated(() => {
      console.log('updated')
    })

    onDestroyed(() => {
      console.log('destroyed')
    })
  }
}

When called inside a component's data() or lifecycle hooks, the current instance is automatically inferred. The instance is also passed into the callback as the argument:

onMounted(instance => {
  console.log(instance.$options.name)
})

Explicit Target Instance

When used outside lifecycle hooks, the target instance can be explicitly passed in via the second argument:

onMounted(() => { /* ... */ }, targetInstance)

If the target instance cannot be inferred and no explicit target instance is passed, an error will be thrown.

Injection Removal

onXXX calls return a removal function that removes the injected hook:

// an updated hook that fires only once
const remove = onUpdated(() => {
  remove()
})

Appendix: More Examples

Pre-requisite: please read the Advanced Reactivity API RFC first.

When combined with the ability to create and observe state via standalone APIs, it's possible to encapsulate arbitrarily complex logic in an external function, (with capabilities similar to React hooks):

Data Fetching

This is the equivalent of what we can currently achieve via scoped slots with libs like vue-promised. This is just an example showing that this new set of API is capable of achieving similar results.

import { value, computed, watch } from 'vue'

function useFetch(endpointRef) {
  const res = value({
    status: 'pending',
    data: null,
    error: null
  })

  // watch can directly take a computed ref
  watch(endpointRef, endpoint => {
    let aborted = false
    fetch(endpoint)
      .then(res => res.json())
      .then(data => {
        if (aborted) {
          return
        }
        res.value = {
          status: 'success',
          data,
          error: null
        }
      }).catch(error => {
        if (aborted) {
          return
        }
        res.value = {
          status: 'error',
          data: null,
          error
        }
      })
    return () => {
      aborted = true
    }
  })

  return res
}

// usage
const App = {
  props: ['id'],
  data() {
    return {
      postData: useFetch(computed(() => `/api/posts/${this.id}`))
    }
  },
  template: `
    <div>
      <div v-if="postData.status === 'pending'">
        Loading...
      </div>
      <div v-else-if="postData.status === 'success'">
        {{ postData.data }}
      </div>
      <div v-else>
        {{ postData.error }}
      </div>
    </div>
  `
}

Use the Platform

Hypothetical examples of exposing state to the component from external sources. This is more explicit than using mixins regarding where the state comes from.

import { value, onMounted, onDestroyed } from 'vue'

function useMousePosition() {
  const x = value(0)
  const y = value(0)

  const onMouseMove = e => {
    x.value = e.pageX
    y.value = e.pageY
  }

  onMounted(() => {
    window.addEventListener('mousemove', onMouseMove)
  })

  onDestroyed(() => {
    window.removeEventListener('mousemove', onMouseMove)
  })

  return { x, y }
}

export default {
  data() {
    const { x, y } = useMousePosition()
    return {
      x,
      y,
      // ... other data
    }
  }
}

Class API

This is a reference of the current implemented version and open to discussion.

Update: don't know since when but latest Chrome Canary now enables class fields by default, so we can already play with the following API without a transpiler.

Plain ES usage:

import { h, Component } from '@vue/renderer-dom'

class App extends Component {
  static props = {
    msg: String
  }

  // data
  count = 0

  // lifecycle
  created() {
    console.log(this.count)
  }

  // getters are converted to computed properties
  get plusOne() {
    return this.count + 1
  }
  
  // a method
  increment() {
    this.count++
  }

  render(props) {
    return h('div', [
      h('span', this.count),
      h('span', props.msg,
      h('button', {
        // methods are auto-bound when they are accessed via the render proxy
        onClick: this.increment
      }, 'increment')
    ])
  }
}

TS Usage:

import { h, Component, ComponentWatchOptions } from '@vue/renderer-dom'

interface Props {
  msg: string
}

interface Data {
  count: number
}

class App extends Component<Props, Data> {
  static props = {
    msg: String
  }
  
  // data fields
  count: number = 0

  // ComponentWatchOptions type is only needed if `this` inference
  // is needed inside options, e.g. in watch callbacks
  static watch: ComponentWatchOptions<App> {
    count(value) {
      console.log(value)
    }
  }

  created() {
    console.log(this.count)
  }

  get plusOne() {
    return this.count + 1
  }

  increment() {
    this.count++
  }

  render(props) {
    // ...
  }
}

TypeScript-specific feedback & thoughts

I'd like to keep the other breaking changes feedback thread scoped to breaking changes. Here's some TypeScript-specific feedback which doesn't need to be addressed immediately, but which stood out to me while exploring the package.

Generic type parameter for props (P) should come before data (D)

This was the biggest footgun I made when I worked on the Vue .d.ts files the first time. I specified Props first rather than Data and I believe that Vue 3.0 shouldn't make the same mistake. When it comes to components, almost all of them will use props and have a harder time specifying them than data.

Generic type parameter P has defaults

There are a lot of instances of P = Data (which is really just P = Record<string, any>). Is there any reason that's necessary? I feel like the class-based API doesn't really need this since, on the whole, it's mostly inferred, though I could be missing something.

Vue is currently non-generic

Vue (from packages/vue) currently has a different API than Component (from packages/core). Specifically, Vue is generic and seems to be the "full-featured" version of the component. You already mentioned that things are in a state of progress, but is the difference mostly about mount points depending on target runtimes (i.e. DOM vs. native vs. ...)?

.d.ts files are produced using dts-bundle

dts-bundle throws all of your modules into a single global file with several ambient module declarations. The problem with ambient module declarations is that they are in the global scope in the first place. If you have multiple versions of Vue loaded up by a project (via a separate dependency probably), then two ambient modules for the path @vue/renderer-dom can conflict, which can cause problems.

Mitigations

We should consider looking into API Extractor to produce .d.ts files that describe top-level APIs for Vue.

Build process could leverage project references.

Right now it appears that there's a custom TypeScript build that happens, and it seems like that's an all-or-nothing approach. Depending on whether build times are an issue, we could potentially leverage project references. If there's a reason you can't use them, it'd be helpful for our team to get an idea of the challenges. ๐Ÿ˜ƒ

Adding a default render function that renders null for components

Right now, the pattern that's been called "renderless components" doesn't make complete sense, because technically, those components still need a render function that just returns null:

render: () => null

What are thoughts on removing the need for either a template or render function in components, by default rendering null?

Difficulty documenting Vue 3 and avoiding a schism

The Problem

Many on the team would like the functions API to become the new recommended way of writing components in Vue 3. And even if we did not officially recommend it as the standard, many users would still gravitate toward it for its organizational and compositional advantages. That means a schism in the community is inevitable with the current API.

I've also been experimenting with how we might document Vue 3 and have been really struggling. I think I finally have to admit that with the current, planned API, making Vue feel as simple and elegant as it does now is simply beyond my abilities.

Proposed solution

I've been experimenting with potential changes to the API, outlining affected examples to create a gentler and more intuitive learning path. My goals are to:

  • Make the recommended API feel intuitive and familiar.
  • Make changes we're making feel like a logical simplification or extension of our current strategy, rather than a radical change in direction.
  • Reduce the feeling of there being two different ways of writing components.
  • Reduce the number of concepts users have to learn.
  • Reduce the frequency and severity of context switching.

Finally, I think have a potential learning path that is worth attention from the team, though you may want to read my explanations for the proposed API changes before checking it out.

Proposed API changes

1. Rename setup to create

Example

Vue.component('button-counter', {
  props: ['initialCount'],
  create(props) {
    return {
      count: props.initialCount
    }
  },
  template: `
    <button v-on:click="count++">
      You clicked me {{ count }} times.
    </button>
  `
})

Advantages

  • Avoid introducing a new concept with setup, since we already have a concept of instance creation with the beforeCreate and created lifecycle functions.

  • With create, it's more obvious and easier to remember when in the lifecycle the function is called.

  • Since this is first available in created, it will make more intuitive sense that it's not yet available in the create function.

Disadvantages

  • Downsides related to autocomplete and confusion with lifecycle functions can be resolved with proposal 2.

2. Rename lifecycle function options to begin with on

Example

new Vue({
  el: '#app',
  onCreated() {
    console.log(`I've been created!`)
  }
})

Advantages

  • Removes any confusion or autocomplete conflicts if setup is renamed to create (see proposal 1).

  • Removes the only naming inconsistency between the option and function versions of an API. For example, computed and watch correspond to Vue.computed and Vue.watch, but created and mounted correspond to Vue.onCreated and Vue.onMounted.

  • Users only have to make the context switch once, when going from Vue 2 to Vue 3, rather than every time they move between options and functions.

  • Better intellisense for lifecycle functions, because when users type the on in import { on } from 'vue', they'll see a list of all available lifecycle functions.

Disadvantages

  • None that I can see.

3. Consolidate 2.x data/computed/methods into create and allow its value to be an object just like data currently

Example

const app = new Vue({
  el: '#app',
  create: {
    count: 0,
    doubleCount: Vue.computed(() =>
      return app.count * 2
    ),
    incrementCount() {
      app.count++
    }
  }
})
Vue.component('button-counter', {
  props: ['initialCount'],
  create(props) {
    const state = {
      count: props.initialCount,
      countIncrease: Vue.computed(
        () => state.count - props.initialCount
      ),
      incrementCount() {
        state.count++
      }
    }

    return state
  },
  template: `
    <button v-on:click="incrementCount">
      You clicked me {{ count }}
      ({{ initialCount }} + {{ countIncrease }})
      times.
    </button>
  `
})

Advantages

  • It's easier for users to remember which options add properties to the instance, since there would only be one: create.

  • Users don't need to be more advanced to better organize their properties. This one change provides the vast majority of the organizational benefit, without the complexity that can arise once you get into advanced composition.

  • New users won't have to learn methods as a separate concept - they're just functions.

  • It's even less code and fewer concepts than the current status quo.

  • Prevents the larger rift of people using create vs data/computed/methods, by having everyone start with create from the beginning. With everyone already familiar with and using the create function, sometimes moving more options there for organization purposes (e.g. watch, onMounted, etc) will be a dramatically smaller leap.

  • Makes the transition to a create function feel more natural, both for current users ("oh, it's just like data - when it's a function, I just return the object") and new users ("oh, this is just like what I was doing before, except I return the object").

  • Although Vue.computed will return a binding, users won't have to worry about learning the concept of bindings for the entirety of Essentials. Only once we get to advanced composition that splits features into reusable functions will it become relevant, because then you have to worry about whether you're passing a value or binding.

Disadvantages

  • If users decided to log a computed property (e.g. console.log(state.countIncrease)) inside the create function, they would see an object with a value rather than the value directly. They won't understand exactly why Vue.computed returns this until they're introduced to bindings, but I don't see it as a significant problem because it won't stop them from getting their work done.

  • When doing something very strange, like trying to immediately use the setter on a computed property inside create, the abstraction of a binding would leak. However, if we think this is likely to actually happen, I believe it could be resolved by emitting a warning on the setter of bindings, since I believe that's always likely to be a mistake.

    const state = {
      count: 0,
      doubleCount: Vue.computed(
        () => state.count * 2,
        newValue => {
          state.count = newValue / 2
        }
      )
    }
    
    // This will not work, because `doubleCount` has not yet
    // been normalized to a reactive property on `state`.
    state.doubleCount = 2
    
    return state

4. Make context the same exact object as this in other options

Example

Vue.component('button-counter', {
  create(props, context) {
    return {
      map: context.$parent.map
    }
  },
  onCreated() {
    console.log(this.$parent.map)
  },
  template: '...'
})
Vue.component('username-input', {
  create(props, context) {
    return {
      focus() {
        context.$refs.input.focus()
      }
    }
  },
  onMounted() {
    console.log(this.$refs.input)
  },
  template: '...'
})

Advantages

  • Avoids a context switch (no pun intended ๐Ÿ˜„) when moving between options and the create function, because properties are accessed under the same name (e.g. this.$refs/context.$refs instead of this.$refs/context.refs).

  • When users/plugins add properties to the prototype or to this in onBeforeCreate, users can rely on those properties being available on the context object in create.

Disadvantages

  • Requires extra documentation to help people understand that this === context, and that properties are added/populated at different points in the lifecycle (e.g. props and state added in onCreated, $refs populated in onMounted, etc). We'll probably need a more detailed version of the lifecycle diagram with these details (or whatever the reality ends up being).

  • For TypeScript users, every plugin that adds properties to the prototype (e.g. $router, $vuex) would require extending the interface of the Vue instance. I think they probably want to do this already for render functions though, right? Don't they still have access to this?

5. Reconsider staying consistent with object-based syntax in the arguments for the function versions of the API?

This one is more of a question than an argument. We have a lot of inconsistencies between the object-based and function-based APIs. For example, when a computed property takes a setter, it's a second argument:

Vue.computed(
  () => {}, // getter
  () => {} // setter
)

rather than providing an object with get and set, like this:

Vue.computed({
  get() {},
  set() {}
})

It's my understanding that these changes were made for the performance benefits of monomorphism. However, they have some significant disadvantages from a human perspective:

  • They force users to learn two versions of every API, rather than being able to mostly copy/paste when refactoring between options and functions, creating more work and making them feel like a significant context switch.

  • They create code that's less explicit and less readable, since either intellisense or comments are necessary to provide more information on what these arguments actually do.

As a starting place, could we create some benchmarks from realistic use cases so we can see exactly how much extra performance we're getting from monomorphism? That could make it easier to judge the pros and cons.

Thoughts?

@vuejs/collaborators What does everyone think about these? They include some big changes, but I think they would vastly simplify the experience of learning and using Vue. I'm also very open to alternatives I may have missed!

Standardizing language for state/data/observable

Since data in components, state in Vuex, and Vue.observable are all used to register observable state objects, I wonder if we should standardize the language we use to reduce the number of concepts people have to learn. My personal preference would be to settle on state, since this is also the word generally preferred in the React and Angular ecosystems, thus easing migration. So the total renames would be:

  • data/$data -> state/$state
  • Vue.observable -> Vue.state

The obvious downside is the adjustment pain for current users, but I think that could be mostly alleviated by a period of allowing the old names to work as aliases, while emitting a warning if they're actually used.

Support passing down slots with v-bind like in 2.x

What problem does this feature solve?

vuejs/vue#7178

It seems like this still needs to be implemented, in v3 because currently it passes slots as component's prop instead of its children.

https://vue-next-template-explorer.netlify.com/#%7B%22src%22%3A%22%3Cchild%20v-bind%3D%5C%22%7B%20slots%3A%20%24slots%20%7D%5C%22%3E%3C%2Fchild%3E%22%2C%22options%22%3A%7B%22mode%22%3A%22module%22%2C%22prefixIdentifiers%22%3Afalse%2C%22hoistStatic%22%3Atrue%2C%22cacheHandlers%22%3Atrue%2C%22scopeId%22%3Anull%7D%7D

What does the proposed API look like?

<child v-bind="{ slots: $slots }"></child>

Template compiler 2.x incompatibility: multiple statements inside event handler has invalid generated code

Version

3.0.0-alpha.1

Reproduction link

(None)

Steps to reproduce

Attempt to compile this template:

<button @click="foo(); bar()">Click</button>

What is expected?

It should compile without any errors or warnings and when the button is clicked it should call the foo and bar functions sequentially.

What is actually happening?

The generated code is invalid.


The following template fragment works in 2.x:

<button @click="foo(); bar()">Click</button>

The click event handler gets compiled into something like this:

click: $event => { foo(); bar() }

But in 3.0.0-alpha.1 the compiled code is this:

click: _cache[0] || (_cache[0] = $event => (foo(); bar()))

which is invalid JS syntax.

It can be fixed on the user's end by:

  • Not having code like that in the template and if you need to execute multiple statements it should be extracted into a method.
  • Using a comma instead of a semicolon.

How to manually unmount Vue from the dom element components

What problem does this feature solve?

  const Vue2ReactWrapper = (vueComponentFactory, componentName) => {
    const ReactPage: React.FC = () => {
      const [loading, setLoading] = useState(true);
      const containerRef = useRef(null);
      useEffect(() => {
        let Vue = require('vue');
        Vue = Vue.default || Vue;
        if (containerRef.current) {
          let vueInstance;
          (async () => {
            let vueApp = await vueComponentFactory();
            vueApp = vueApp.default || vueApp;
            const { createApp } = Vue;
            vueInstance = createApp().mount(vueApp, containerRef.current);
            // vueInstance = new Vue({
            //   render: h => h(vueApp),
            // }).$mount(containerRef.current);
            setLoading(false);
          })();
          return () => {
            if(!vueInstance) return;
            if (vueInstance.$destroy) {
              vueInstance.$destroy();
            } else if (vueInstance.sink) {
              vueInstance.sink.renderer.unmount(vueInstance.$root.vnode);
            }
          }
        }
      }, []);
      ReactPage.displayName = componentName;
      return (
        <div>
          <div ref={containerRef} />
          {loading && <span> ้กต้ขๅŠ ่ฝฝไธญ </span>}
        </div>
      );
    }
    return ReactPage;
  }

ๆˆ‘ๅธŒๆœ›ๅœจ React ๅ…ƒ็ด  unmount ๆ—ถ ๏ผŒVue ๅ…ƒ็ด ไนŸ่ƒฝๅคŸ unmountใ€‚
I hope in the React unmount to elements, also able to unmount to Vue elements.

but now vueInstance.$destroy is undefined...

What does the proposed API look like?

vueInstance.$destroy()

release resources๏ผŒcall beforeUnMountใ€

Progress Tracking

Observer

  • Object
  • Array
  • Collections
    • Map
    • WeakMap
    • Set
    • WeakSet
  • Immutable
  • Computed
  • Advanced Reactivity API (vuejs/rfcs#22)
    • value
    • state / value.raw?
    • computed
    • watch
    • setup() hook

Core

  • Custom Renderer API - createRenderer

  • Component

  • Portal

  • Fragments

  • ref

  • Directives

  • Options

    • props
      • immutable when passed down
      • runtime validation
    • render
  • Global API

    • createApp
      • app.config
        • performance
        • devtools
        • errorHandler
        • warnHandler
        • ignoredElements
        • keyCodes
        • optionMergeStrategies
        • productionTip only show in direct browser builds
        • silent deprecated
      • app.use
      • app.mixin (only for legacy mode)
      • app.directive
      • app.component
    • nextTick
    • compile
    • version
  • Lifecycle hooks

    • beforeMount
    • mounted
    • beforeUpdate
    • updated
    • beforeUnmount
    • unmounted
    • errorCaptured
    • renderTracked
    • renderTriggered
  • Instance properties

    • $data
    • $props
    • $attrs
    • $slots
    • $refs
    • $root
    • $parent
    • $children
    • $options
    • $el
  • Instance methods

    • $forceUpdate

Compiler

  • parser
    • HTML parser
    • Vue parser
    • AST types
  • optimizer
  • codegen
    • tree-shaking mode
    • source maps
  • Plugin system
  • Slots
  • <component is="">
  • Directives
    • v-if / v-else / v-else-if
    • v-for
    • v-on
    • v-bind
    • v-model
    • v-show
    • v-pre
    • v-cloak
    • v-once
    • v-text
    • v-html
  • string ref -> function ref

On demand features

  • createAsyncComponent
  • <transition>
  • <transition-group>
  • <keep-alive>
    • activated
    • deactivated
  • <await>

SSR

  • SSR optimizing compiler
  • server-renderer
    • string
    • stream
      • embed state associated with component as inline <script>
    • special handling for Async components
  • Client hydration
  • $isServer instance property

2.x Compat

  • Global config
  • Global API
    • Vue.extend
    • Vue.nextTick
    • Vue.use
    • Vue.compile
    • Vue.version
    • Vue.set
    • Vue.delete
    • Vue.component
    • Vue.directive
    • Vue.filter
  • Options
    • data
    • methods
    • computed
      • get
      • set
      • chained
    • watch
      • getter
      • dot-delimited path
      • deep
      • sync
      • immediate
    • template
    • functional
    • el
    • propsData
    • parent
    • mixins
    • extends
    • model
    • comments
    • provide/inject
    • renderError
    • inheritAttrs
  • Legacy Lifecycle Hooks
    • beforeCreate
    • created
  • Instance Properties
    • $scopedSlots
    • $listeners
  • Instance methods
    • $watch
    • $nextTick
    • $mount
    • $destroy
    • $on
    • $once
    • $off
    • $emit

Attribute Fallthrough Behavior

Draft based on #5


  • Start Date: 2019-03-15
  • Target Major Version: 3.x
  • Reference Issues: N/A
  • Implementation PR: N/A

Summary

  • Disable implicit attribute fall-through to child component root element

  • Remove inheritAttrs option

Basic example

To replicate 2.x behavior in templates:

<div v-bind="$attrs">hi</div>

In render function:

import { h } from 'vue'

export default {
  render() {
    return h('div', this.$attrs, 'hi')
  }
}

Motivation

In 2.x, the current attribute fallthrough behavior is quite implicit:

  • class and style used on a child component are implicitly applied to the component's root element. It is also automatically merged with class and style bindings on that element in the child component template.

    • However, this behavior is not consistent in functional components because functional components may return multiple root nodes.

    • With 3.0 supporting fragments and therefore multiple root nodes for all components, this becomes even more problematic. The implicit behavior can suddenly fail when the child component changes from single-root to multi-root.

  • attributes passed to a component that are not declared by the component as props are also implicitly applied to the component root element.

    • Again, in functional components this needs explicit application, and would be inconsistent for 3.0 components with multiple root nodes.

    • this.$attrs only contains attributes, but excludes class and style; v-on listeners are contained in a separate this.$listeners object. There is also the .native modifier. The combination of inheritAttrs, .native, $attrs and $listeners makes props passing in higher-order components unnecessarily complex. The new behavior makes it much more straightforward: spreading $attrs means "pass everything that I don't care about down to this element/component".

    • class and style are always automatically merged, and are not affected by inheritAttrs.

The fallthrough behavior has already been inconsistent between stateful components and functional components in 2.x. With the introduction of fragments (the ability for a component to have multiple root nodes) in 3.0, the fallthrough behavior becomes even more unreliable for component consumers. The implicit behavior is convenient in cases where it works, but can be confusing in cases where it doesn't.

In 3.0, we are planning to make attribute fallthrough an explicit decision of component authors. Whether a component accepts additional attributes becomes part of the component's API contract. We believe overall this should result in a simpler, more explicit and more consistent API.

Detailed design

  • inheritAttrs option will be removed.

  • .native modifier will be removed.

  • Non-prop attributes no longer automatically fallthrough to the root element of the child component (including class and style). This is the same for both stateful and functional components.

    This means that with the following usage:

    const Child = {
      props: ['foo'],
      template: `<div>{{ foo }}</div>`
    }
    
    const Parent = {
      components: { Child },
      template: `<child foo="1" bar="2" class="bar"/>`
    }

    Both bar="2" AND class="bar" on <child> will be ignored.

  • this.$attrs now contains everything passed to the component except those that are declared as props or custom events. This includes class, style, v-on listeners (as onXXX properties). The object will be flat (no nesting) - this is possible thanks to the new flat VNode data structure (discussed in Render Function API Change).

    To explicitly inherit additional attributes passed by the parent, the child component should apply it with v-bind:

    const Child = {
      props: ['foo'],
      template: `<div v-bind="$attrs">{{ foo }}</div>`
    }

    This also applies when the child component needs to apply $attrs to a non-root element, or has multiple root nodes:

    const ChildWithNestedRoot = {
      props: ['foo'],
      template: `
        <label>
          {{ foo }}
          <input v-bind="$attrs">
        </label>
      `
    }
    
    const ChildWithMultipleRoot = {
      props: ['foo'],
      template: `
        <label :for="$attrs.id">{{ foo }}</label>
        <input v-bind="$attrs">
      `
    }

    In render functions, if simple overwrite is acceptable, $attrs can be merged using object spread. But in most cases, special handling is required (e.g. for class, style and onXXX listeners). Therefore a cloneVNode helper will be provided. It handles the proper merging of VNode data:

    import { h, cloneVNode } from 'vue'
    
    const Child = {
      render() {
        const inner = h(InnerComponent, {
          foo: 'bar'
        })
        return cloneVNode(inner, this.$attrs)
      }
    }

    The 2nd argument to cloneVNode is optional. It means "clone the vnode and add these additional props". The cloneVNode helper serves two purposes:

    • Avoids mutating the original VNode
    • Handles special merging logic for class, style and event listeners

    Inside render functions, the user also has the full flexibility to pluck / omit any props from $attrs using 3rd party helpers, e.g. lodash.

Removing Unwanted Listeners

With flat VNode data and the removal of .native modifier, all listeners are passed down to the child component as onXXX functions:

<foo @click="foo" @custom="bar" />

compiles to:

h(foo, {
  onClick: foo,
  onCustom: bar
})

When spreading $attrs with v-bind, all parent listeners are applied to the target element as native DOM listeners. The problem is that these same listeners can also be triggered by custom events - in the above example, both a native click event and a custom one emitted by this.$emit('click') in the child will trigger the parent's foo handler. This may lead to unwanted behavior.

Props do not suffer from this problem because declared props are removed from $attrs. Therefore we should have a similar way to "declare" emitted events from a component. There is currently an open RFC for it by @niko278.

Event listeners for explicitly declared events will be removed from $attrs and can only be triggered by custom events emitted by the component via this.$emit.

Drawbacks

  • Fallthrough behavior is now disabled by default and is controlled by the component author. If the component is intentionally "closed" there's no way for the consumer to change that. This may cause some inconvenience for users accustomed to the old behavior, especially when using class and style for styling purposes, but it is the more "correct" behavior when it comes to component responsibilities and boundaries. Styling use cases can be easily worked around with by wrapping the component in a wrapper element. In fact, this should be the best practice in 3.0 because the child component may or may not have multiple root nodes.

  • For accessibility reasons, it should be a best practice for components that are shipped as libraries to always spread $attrs so that any aria-x attributes can fallthrough. However this is a straightforward / mechanical code change, and is more of an educational issue. We could make it common knowledge by emphasizing this in all our information channels.

Alternatives

N/A

Adoption strategy

Documentation

This RFC discusses the problem by starting with the 2.x implementation details with a lot of history baggage so it can seem a bit complex. However if we were to document the behavior for a new user, the concept is much simpler in comparison:

  • For a component without explicit props and events declarations, everything passed to it from the parent ends up in $attrs.

  • If a component declares explicit props, they are removed from $attrs.

  • If a component declares explicit events, corresponding onXXX listeners are removed from $attrs.

  • $attrs essentially means extraneous attributes,, or "any attributes passed to the component that hasn't been explicitly handled by the component".

Migration

This will be one of the changes that will have a bigger impact on existing code and would likely require manual migration.

  • We will provide a warning when a component has unused extraneous attributes (i.e. non-empty $attrs that is never used during render).

  • For application code that adds class / style to child components for styling purposes: the child component should be wrapped with a wrapper element.

  • For higher-order components or reusable components that allow the consumer to apply arbitrary attributes / listeners to an inner element (e.g. custom form components that wrap <input>):

    • Declare props and events that are consumed by the HOC itself (thus removing them from $attrs)

    • Refactor the component and explicitly add v-bind="$attrs" to the target inner component or element. For render functions, apply $attrs with the cloneVNode helper.

    • If a component is already using inheritAttrs: false, the migration should be relatively straightforward.

We will need more dogfooding (migrating actual apps to 3.0) to provide more detailed migration guidance for this one, since the migration cost heavily depends on usage.

Advanced Reactivity API

Review note: I've split this part out of the React hooks like composition API because it is not strictly coupled to that proposal.


  • Start Date: 03-05-2019
  • Target Major Version: 2.x & 3.x
  • Reference Issues: N/A
  • Implementation PR: N/A

Summary

Provide standalone APIs for creating and observing reactive state.

Basic example

import { state, value, computed, watch } from '@vue/observer'

// reactive object
// equivalent of 2.x Vue.observable()
const obj = state({ a: 1 })

// watch with a getter function
watch(() => obj.a, value => {
  console.log(`obj.a is: ${value}`)
})

// a "ref" object that has a .value property
const count = value(0)

// computed "ref" with a read-only .value property
const plusOne = computed(() => count.value + 1)

// refs can be watched directly
watch(count, (count, oldCount) => {
  console.log(`count is: ${count}`)
})

watch(plusOne, countPlusOne => {
  console.log(`count plus one is: ${countPlusOne}`)
})

Motivation

Decouple the reactivity system from component instances

Vue's reactivity system powers a few aspects of Vue:

  • Tracking dependencies used during a component's render for automatic component re-render

  • Tracking dependencies of computed properties to only re-compute values when necessary

  • Expose this.$watch API for users to perform custom side effects in response to state changes

Until 2.6, the reactivity system has largely been considered an internal implementation, and there is no dedicated API for creating / watching reactive state without doing it inside a component instance.

However, such coupling isn't technically necessary. In 3.x we've already split the reactivity system into its own package (@vue/observer) with dedicated APIs, so it makes sense to also expose these APIs to enable more advanced use cases.

With these APIs it becomes possible to encapsulate stateful logic and side effects without components involved. In addition, with proper ability to "connect" the created state back into component instances, they also unlock a powerful component logic reuse mechanism.

Detailed design

Reactive Objects

In 2.6 we introduced the observable API for creating reactive objects. We've noticed the naming causes confusion for some users who are familiar with RxJS or reactive programming where the term "observable" is commonly used to denote event streams. So here we intend to rename it to simply state:

import { state } from 'vue'

const object = state({
  count: 0
})

This works exactly like 2.6 Vue.observable. The returned object behaves just like a normal object, and when its properties are accessed in reactive computations (render functions, computed property getters and watcher getters), they are tracked as dependencies. Mutation to these properties will cause corresponding computations to re-run.

Value Refs

The state API cannot be used for primitive values because:

  • Vue tracks dependencies by intercepting property accesses. Usage of primitive values in reactive computations cannot be tracked.

  • JavaScript values are not passed by reference. Passing a value directly means the receiving function will not be able to read the latest value when the original is mutated.

The simple solution is wrapping the value in an object wrapper that can be passed around by reference. This is exactly what the value API does:

import { value } from 'vue'

const countRef = value(0)

The value API creates a wrapper object for a value, called a ref. A ref is a reactive object with a single property: .value. The property points to the actual value being held and is writable:

// read the value
console.log(countRef.value) // 0

// mutate the value
countRef.value++

Refs are primarily used for holding primitive values, but it can also hold any other values including deeply nested objects and arrays. Non-primitive values held inside a ref behave like normal reactive objects created via state.

Computed Refs

In addition to plain value refs, we can also create computed refs:

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

Computed refs are readonly by default - assigning to its value property will result in an error.

Computed refs can be made writable by passing a write callback as the 2nd argument:

const writableRef = computed(
  // read
  () => count.value + 1,
  // write
  val => {
    count.value = val - 1
  }
)

Computed refs behaves like computed properties in a component: it tracks its dependencies and only re-evaluates when dependencies have changed.

Watchers

All .value access are reactive, and can be tracked with the standalone watch API.

NOTE: unlike 2.x, the watch API is immediate by default.

watch can be called with a single function. The function will be called immediately, and will be called again whenever dependencies change:

import { value, watch } from 'vue'

const count = value(0)

// watch and re-run the effect
watch(() => {
  console.log('count is: ', count.value)
})
// -> count is: 0

count.value++
// -> count is: 1

Watch with a Getter

When using a single function, any reactive properties accessed during its execution are tracked as dependencies. The computation and the side effect are performed together. To separate the two, we can pass two functions instead:

watch(
  // 1st argument (the "computation", or getter) should return a value
  () => count.value + 1,
  // 2nd argument (the "effect", or callback) only fires when value returned
  // from the getter changes
  value => {
    console.log('count + 1 is: ', value)
  }
)
// -> count + 1 is: 1

count.value++
// -> count + 1 is: 2

Watching Refs

The 1st argument can also be a ref:

// double is a computed ref
const double = computed(() => count.value * 2)

// watch a ref directly
watch(double, value => {
  console.log('double the count is: ', value)
})
// -> double the count is: 0

count.value++
// -> double the count is: 2

Stopping a Watcher

A watch call returns a stop handle:

const stop = watch(...)

// stop watching
stop()

If watch is called inside lifecycle hooks or data() of a component instance, it will automatically be stopped when the associated component instance is unmounted:

export default {
  created() {
    // stopped automatically when the component unmounts
    watch(() => this.id, id => {
      // ...
    })
  }
}

Effect Cleanup

The effect callback can also return a cleanup function which gets called every time when:

  • the watcher is about to re-run
  • the watcher is stopped
watch(idRef, id => {
  const token = performAsyncOperation(id)

  return () => {
    // id has changed or watcher is stopped.
    // invalidate previously pending async operation
    token.cancel()
  }
})

Non-Immediate Watchers

To make watchers non-immediate like 2.x, pass additional options via the 3rd argument:

watch(
  () => count.value + 1,
  () => {
    console.log(`count changed`)
  },
  { immediate: false }
)

Exposing Refs to Components

While this proposal is focused on working with reactive state outside of components, such state should also be usable inside components as well.

Refs can be returned in a component's data() function:

import { value } from 'vue'

export default {
  data() {
    return {
      count: value(0)
    }
  }
}

When a ref is returned as a root-level property in data(), it is bound to the component instance as a direct property. This means there's no need to access the value via .value - the value can be accessed and mutated directly as this.count, and directly as count inside templates:

<div @click="count++">
  {{ count }}
</div>

Beyond the API

The APIs proposed here are just low-level building blocks. Technically, they provide everything we need for global state management, so Vuex can be rewritten as a very thin layer on top of these APIs. In addition, when combined with the ability to programmatically hook into the component lifecycle, we can offer a logic reuse mechanism with capabilities similar to React hooks.

Drawbacks

  • To pass state around while keeping them "trackable" and "reactive", values must be passed around in the form of ref containers. This is a new concept and can be a bit more difficult to learn than the base API. However, these APIs are intended for advanced use cases so the learning cost should be acceptable.

Alternatives

N/A

Adoption strategy

This is mostly new APIs that expose existing internal capabilities. Users familiar with Vue's existing reactivity system should be able to grasp the concept fairly quickly. It should have a dedicated chapter in the official guide, and we also need to revise the Reactivity in Depth section of the current docs.

Unresolved questions

  • watch API overlaps with existing this.$watch API and watch component option. In fact, the standalone watch API provides a superset of existing APIs. This makes the existence of all three redundant and inconsistent.

    Should we deprecate this.$watch and watch component option?

    Sidenote: removing this.$watch and the watch option also makes the entire watch API completely tree-shakable.

  • We probably need to also expose a isRef method to check whether an object is a value/computed ref.

Moving v-bind.sync to the v-model API

@yyx990803 I've seen v-bind.sync cause quite a bit of confusion in Vue 2, as users expect it to be able to use expressions like with v-bind (despite whatever we put in the docs). The explanation I've had the best success with is:

Thinking about :title.sync="title" like a normal binding with extra behavior is really the wrong way to think about it, because two-way bindings are fundamentally different. The .sync modifier works essentially like v-model, which is Vue's other syntax sugar for creating a two-way binding. The main difference is that it expands to a slightly different pattern that allows you to have multiple two-way bindings on a single component, rather than being limited to just one.

Which brings me to the question: if it helps to tell users not to think of v-bind.sync like v-bind, but rather to think about it like v-model, should it be part of the v-model API instead? For example, instead of:

<MyComponent v-bind:title.sync="title" />

Perhaps a more intuitive syntax would be:

<MyComponent v-model:title="title" />

As a bonus, a change like this would be very easy to flag with the migration helper and fix with find-and-replace.

Thoughts?

Should unique keys automatically be added to v-if/v-else-if/v-else groups?

Currently, we provide warnings like this one in several places of the docs. However, I think it might make more sense to automatically add unique keys to unkeyed conditional elements, since it's very rare to need elements with the same tag name to be reused.

The only potential disadvantages that come to mind are:

  • This is one of the few breaking changes we wouldn't automatically be able to fix for people with the migration helper, since we can't know the intent of the code.
  • Some apps would take a tiny performance hit, but I expect it would be negligible and they'd be able to easily fix this by explicitly adding the same key to each conditional element.
  • This could be seen as slightly magical, since the default behavior would be different in render functions. I personally find this acceptable, since render functions are already more manual in other ways.

v-model changes

  • Start Date: 2019-04-09
  • Target Major Version: 3.x
  • Reference Issues: vuejs/rfcs#8
  • Implementation PR: N/A

Summary

Adjust v-model API when used on custom components for more flexible usage, and adjust compilation output on native elements for more succinct compiler output.

This builds on top of vuejs/rfcs#8 (Replace v-bind's .sync with a v-model argument).

Basic example

Motivation

Previously, v-model="foo" on components roughly compiles to the following:

h(Comp, {
  value: foo,
  onInput: value => {
    foo = value
  }
})

However, this requires the component to always use the value prop for binding with v-model when the component may want to expose the value prop for a different purpose.

In 2.2 we introduced the model component option that allows the component to customize the prop and event to use for v-model. However, this still only allows one v-model to be used on the component. In practice we are seeing some components that need to sync multiple values, and the other values have to use v-bind.sync. We noticed that v-model and v-bind.sync are fundamentally doing the same thing and can be combined into a single construct by allowing v-model to accept arguments (as proposed in vuejs/rfcs#8).

Detailed design

In 3.0, the model option will be removed. v-model="foo" (without argument) on a component compiles to the following instead:

h(Comp, {
  modelValue: foo,
  'onUpdate:modelValue': value => {
    foo = value
  }
})

If the component wants to support v-model without an argument, it should expect a prop named modelValue. To sync its value back to the parent, the child should emit an event named "update:modelValue" (see Render Function API change for details on the new VNode data structure).

The default compilation output uses the prop name modelValue so that it is clear this prop is compiled from v-model. This will be useful to differentiate it from the original value prop which could've been created manually by the user (especially in the native element case detailed in a later section).

RFC #8 proposes the ability for v-model to accept arguments. The argument can be used to denote the prop v-model should bind to. v-model:value="foo" compiles to:

h(Comp, {
  value: foo,
  'onUpdate:value': value => {
    foo = value
  }
})

In this case, the child component expects a value prop and emits "update:value" to sync.

Note that this enables multiple v-model bindings on the same component, each syncing a different prop, without the need for extra options in the component:

<InviteeForm
  v-model:name="inviteeName"
  v-model:email="inviteeEmail"
/>

Usage on Native Elements

Another aspect of the v-model usage is on native elements. In 2.x, the compiler produces different code based on the element type v-model is used on. For example, it outputs different prop/event combinations for <input type="text"> and <input type="checkbox">. However, this strategy does not handle dynamic element or input types very well:

<input :type="dynamicType" v-model="foo">

The compiler has no way to guess the correct prop/event combination at compile time, so it has to produce very verbose code to cover possible cases.

In 3.0, v-model on native elements produces the exact same output as when used on components. For example, <input v-model="foo"> compiles to:

h('input', {
  modelValue: foo,
  'onUpdate:modelValue': value => {
    foo = value
  }
})

The idea is to move element/input type specific handling to the runtime. For this reason, the v-model output must be something special (modelValue) for the runtime to pick up and transform. If we use the default value and input, the runtime won't know if it's created by v-model or manually by the user.

The module responsible for patching element props for the web platform will dynamically determine what actual prop/event to bind. For example, on <input type="checkbox">, modelValue will be mapped to checked and "update:modelValue" will be mapped to "change". Moving the logic to runtime allows the framework to handle dynamic cases better, and enables the compiler to output less verbose code.

Drawbacks

TODO

Alternatives

N/A

Adoption strategy

TODO

Unresolved questions

Usage on Custom Elements

It is still difficult to use v-model on native custom elements, since 3rd party custom elements have unknown prop/event combinations and do not necessarily follow Vue's sync event naming conventions. For example:

<custom-input v-model="foo"></custom-input>

Vue has no information on the property to bind to or the event to listen to. One possible way to deal with this is to use the type attribute as a hint:

<custom-input v-model="foo" type="checkbox"></custom-input>

This would tell Vue to bind v-model using the same logic for <input type="checkbox">, using checked as the prop and change as the event.

If the custom element doesn't behave like any existing input type, then it's probably better off to use explicit v-bind and v-on bindings.

Unnecessary import when component contains only static HTML

Version

3.0.0-alpha.1

Reproduction link

https://vue-next-template-explorer.netlify.com/#%7B%22src%22%3A%22%3Cdiv%3Etest%3C%2Fdiv%3E%22%2C%22options%22%3A%7B%22mode%22%3A%22module%22%2C%22prefixIdentifiers%22%3Afalse%2C%22hoistStatic%22%3Afalse%2C%22cacheHandlers%22%3Afalse%2C%22scopeId%22%3Anull%7D%7D

Steps to reproduce

Create a component with a simple <div>.

What is expected?

  • createVNode is not imported,
  • variable _ctx is not declared.

What is actually happening?

Currently, when component contains only static HTML, createVNode is imported even though it is not needed, additionally _ctx variable is created which is also not used.

Should non-API static class properties be reactive?

In a component like this:

class Counter extends Component {
  static count = 0

  count = 0  

  increment() {
    this.count++
    Counter.count++
  }
}

I'm wondering if users might expect Counter.count to be reactive. In this case, the static count might exist to track activity between all instances of the component. The only cases I can think of when users would expect static properties not to be reactive, is when they're part of an API for Vue or a plugin. For example, with vuefire:

static firebase = {
  anArray: db.ref('url/to/my/collection')
}

So this means plugins would be forced to register all the static properties it wants to control, but I think this is actually a good thing. It would allow us to provide explicit warnings in the rare cases when two plugins conflict - or when a plugin tries to use a property that Vue itself has already claimed.

Another caveat is that for templates, which is the only place where these static properties aren't currently available, I think we'd currently have to do something like this:

{{ $options.count }}

But this feels very strange to me, since count isn't really an "option". So at some point, we'll probably get a requested change in scope for templates to:

with ({ [Component.name]: Component }) {
  with (this) {
    // compiled template
  }
}

so that users could access the static count in templates just like in JavaScript, with:

{{ Counter.count }}

That actually sounds like a good idea to me too, making the experience between JSX and templates a little more universal.

And finally, I do want to acknowledge that there is technically a way to achieve the desired behavior:

const globalData = { totalCount = 0 }

class Counter extends Component {
  globalData = globalData   

  Counter = globalData

  increment() {
    this.count++
    globalData.totalCount++
  }
}

but that seems pretty hacky to me. ๐Ÿ˜…

Optional Props Declaration

  • Start Date: 2019-03-21
  • Target Major Version: 2.x & 3.x
  • Reference Issues: N/A
  • Implementation PR: N/A

Summary

Make component props declaration optional.

Basic example

<!-- valid SFC -->
<template>
  <div>{{ $props.foo }}</div>
</template>

Motivation

In simple use cases where there is no need for runtime props type checking (especially in functional components), making props optional could result in simpler code.

Detailed design

Stateful Components

When a component has no props declarations, all attributes passed by the parent are exposed in this.$props. Unlike declared props, they will NOT be exposed directly on this. In addition, in this case this.$attrs and this.$props will be pointing to the same object.

Nice thing about this is you can omit the <script> block altogether in a simple SFC:

<template>
  <div>{{ $props.foo }}</div>
</template>

Functional Components

This is based on plain-function functional components proposed in Functional and Async Component API Change.

const FunctionalComp = props => {
  return h('div', props.foo)
}

To declare props for plain-function functional components, attach it to the function itself:

FunctionalComp.props = {
  foo: Number
}

Similar to stateful components, when props are declared, the props arguments will only contain the declared props - attributes received but not declared as props will be in the 3rd argument (attrs):

const FunctionalComp = (props, slots, attrs) => {
  // `attrs` contains all received attributes except declared `foo`
}

FunctionalComp.props = {
  foo: Number
}

For mode details on the new functional component signature, see Render Function API Change.

Drawbacks

N/A

Alternatives

N/A

Adoption strategy

The behavior is fully backwards compatible.

Can not use reactive as watchsource

Not sure if this is intended but this code won't work:

watch(reactive({"test": "test"}), () => console.log("foo"))

but for normal refs, this works perfectly fine:

watch(ref(0), () => console.log("foo"))

will reactives also be supported?

Class API RFC Draft (for internal feedback)

  • Start Date: 2019-01-21
  • Target Major Version: 3.x
  • Reference Issues:
  • Implementation PR:

Summary

Introduce built-in support for authoring components as native ES2015 classes.

Basic example

import Vue from 'vue'

export default class App extends Vue {
  // options declared via static properties (stage 3)
  // more details below
  static template = `
    <div>{{ count }}</div>
  `

  // reactive data declared via class fields (stage 3)
  // more details below
  count = 0

  // lifecycle
  created() {
    console.log(this.count)
  }

  // getters are converted to computed properties
  get plusOne() {
    return this.count + 1
  }

  // a method
  increment() {
    this.count++
  }
}

Motivation

Vue's current object-based component API has created some challenges when it comes to type inference. As a result, most users opting into using Vue with TypeScript end up using vue-class-component. This approach works, but with some drawbacks:

  • Internally, Vue 2.x already represents each component instance with an underlying "class". We are using quotes here because it's not using the native ES2015 syntax but the ES5-style constructor/prototype function. Nevertheless, conceptually components are already handled as classes internally.

  • vue-class-component had to implement some inefficient workarounds in order to provide the desired API without altering Vue internals.

  • vue-class-component has to maintain typing compatibility with Vue core, and the maintenance overhead can be eliminated by exposing the class directly from Vue core.

The primary motivation of native class support is to provide a built-in and more efficient replacement for vue-class-component. The affected target audience are most likely also TypeScript users.

The API is also designed to not rely on anything TypeScript specific: it should work equally well in plain ES, for users who prefer using native ES classes.

Note we are not pushing this as a replacement for the existing object-based API - the object-based API will continue to work in 3.0.

Detailed design

Basics

A component can be declared by extending the base Vue class provided by Vue core:

import Vue from 'vue'

class MyComponent extends Vue {}

Data

Reactive instance data properties can be declared using class fields syntax (stage 3):

class MyComponent extends Vue {
  count = 0
}

This is currently supported in Chrome stable 72+ and TypeScript. It can also be transpiled using Babel. If using native ES classes without any transpilation, it's also possible to manually set this.count = 0 in constructor, which would in turn require a super() call:

// NOT recommended.
class MyComponent extends Vue {
  constructor() {
    super()
    this.count = 0
  }
}

This is verbose and also has incorrect semantics (see below). A less verbose alternative is using the special data() method, which works the same as in the object-based syntax:

class MyComponent extends Vue {
  data() {
    return {
      count: 0
    }
  }
}

A Note on [[Set]] vs [[Define]]

The class field syntax uses [[Define]] semantics in both native and transpiled implementations (Babel already conforms to the latest spec and TS will have to follow suite). This means count = 0 in the class body is executed with the semantics of Object.defineProperty and will always overwrite a property of the same name inherited from a parent class, regardless of whether it has a setter or not.

In comparison, this.count = 0 in constructor is using [[Set]] semantics - if the parent class has a defined setter named count, the operation will trigger the setter instead of overwriting the definition.

For Vue's API, [[Define]] is the correct semantics, since an extended class declaring a data property should overwrite a property with the same name on the parent class.

This should be a very rare edge case since most users will likely be using the class field syntax either natively or via a transpiler with correct semantics, or using the data() alternative.

Lifecycle Hooks

Built-in lifecycle hooks should be declared directly as methods, and works largely the same with their object-based counterparts:

class MyComponent extends Vue {
  created() {
    console.log('created')
  }
}

Props

In v3, props declarations can be optional. The behavior will be different based on whether props are declared.

Props with Explicit Declaration

Props can be declared using the props static property (static properties are used for all component options that do not have implicit mapping). When props are declared, they can be accessed directly on this:

class MyComponent extends Vue {
  // props declarations are fully compatible with v2 options
  static props = {
    msg: String
  }

  created() {
    // available on `this`
    console.log(this.msg)

    // also available on `this.$props`
    console.log(this.$props.msg)
  }
}

Similar to v2, any attributes passed to the component but is not declared as a prop will be exposed as this.$attrs. Note that the non-props attribute fallthrough behavior will also be adjusted - it is discussed in more details in a separate RFC.

Props without Explicit Declaration

It is possible to omit props declarations in v3. When there is no explicit props declaration, props will NOT be exposed on this - they will only be available on this.$props:

class MyComponent extends Vue {
  created() {
    console.log(this.$props.msg)
  }
}

Inside templates, the prop also must be accessed with the $props prefix, .e.g. {{ $props.msg }}.

Any attribute passed to this component will be exposed in this.$props. In addition, this.$attrs will be simply pointing to this.$props since they are equivalent in this case.

Computed Properties

Computed properties are declared as getter methods:

class MyComponent extends Vue {
  count = 0

  get doubleCount() {
    return this.count * 2
  }
}

Note although we are using the getter syntax, these functions are not used a literal getters - they are converted into Vue computed properties internally with dependency-tracking-based caching.

Do we need a way to opt-out? It can probably be done via decorators.

Methods

Any method that is not a reserved lifecycle hook is considered a normal instance method:

class MyComponent extends Vue {
  count = 0

  created() {
    this.logCount()
  }

  logCount() {
    console.log(this.count)
  }
}

When methods are accessed from this, they are automatically bound to the instance. This means there is no need to worry about calling this.foo = this.foo.bind(this).

Other Options

Other options that do not have implicit mapping in the class syntax should be declared as static class properties:

class MyComponent extends Vue {
  static template = `
    <div>hello</div>
  `
}

The above syntax requires static class fields (stage 3). In non-supporting environment, manual attaching is required:

class MyComponent extends Vue {}

MyComponent.template = `
  <div>hello</div>
`

Or:

class MyComponent extends Vue {}

Object.assign(MyComponent, {
  template: `
    <div>hello</div>
  `
})

TypeScript Usage

In TypeScript, since data properties are declared using class fields, the type inference just works:

class MyComponent extends Vue {
  count: number = 1

  created() {
    this.count // number
  }
}

For props, we intend to provide a decorator that internally transforms decorated fields in to corresponding runtime options (similar to the @Prop decorator in vue-property-decorators):

import { prop } from '@vue/decorators'

class MyComponent extends Vue {
  @prop count: number

  created() {
    this.count // number
  }
}

This is equivalent to the following in terms of runtime behavior (only static type checking, no runtime checks):

class MyComponent extends Vue {
  static props = ['count']

  created() {
    this.count
  }
}

The decorator can also be called with additional options for more specific runtime behavior:

import { prop } from '@vue/decorators'

class MyComponent extends Vue {
  @prop({
    validator: val => {
      // custom runtime validation logic
    }
  })
  msg: string = 'hello'

  created() {
    this.count // number
  }
}

Note on Prop Default Value

Note that due to the limitations of the TypeScript decorator implementation, we cannot use the following to declare default value for a prop:

class MyComponent extends Vue {
  @prop count: number = 1
}

The culprit is the following case:

class MyComponent extends Vue {
  @prop foo: number = 1
  bar = this.foo + 1
}

If the parent component passes in the foo prop, the default value of 1 should be overwritten. However, the way TypeScript transpiles the code places the two lines together in the constructor of the class, giving Vue no chance to overwrite the default value properly. Vue will throw a warning when such usage is detected.

Instead, use the decorator option to declare default values:

class MyComponent extends Vue {
  @prop({ default: 1 }) foo: number
  bar = this.foo + 1
}

This restriction can be lifted in the future when the ES decorators proposal has been finalized and TS has been updated to match the spec, assuming the final spec does not deviate too much from how it works now.

$props and $data

To access this.$props or this.$data in TypeScript, the base Vue class accepts generic arguments:

interface MyProps {
  msg: string
}

interface MyData {
  count: number
}

class MyComponent extends Vue<MyProps, MyData> {
  count: number = 1

  created() {
    this.$props.msg
    this.$data.count
  }
}

Mixins

Mixins work a bit differently with classes, primarily to ensure proper type inference:

  1. If type inference is needed, mixins must be declared as classes extending the base Vue class (otherwise, the object format also works).

  2. To use mixins, the final component should extend a class created from the mixins method instead of the base Vue class.

import Vue, { mixins } from 'vue'

class MixinA extends Vue {
  // class-style mixin
}

const MixinB = {
  // object-style mixin
}

class MyComponent extends mixins(MixinA, MixinB) {
  // ...
}

The class returned from mixins also accepts the same generics arguments as the base Vue class.

Difference from 2.x Constructors

One major difference between 3.0 classes and the 2.x constructors is that they are not meant to be instantiated directly. i.e. you will no longer be able to do new MyComponent({ el: '#app' }) to mount it - instead, the instantiation/mounting process will be handled by separate, dedicated APIs. In cases where a component needs to be instantiated for testing purposes, corresponding APIs will also be provided. This is largely due to the internal changes where we are moving the mounting logic out of the component class itself for better decoupling, and also has to do our plan to redesign the global API for bootstrapping an app.

Drawbacks

Reliance on Stage 2/3 Language Features

Class Fields

The proposed syntax relies on two currently stage-3 proposals related to class fields:

These are required to achieve the ideal usage. Although there are workarounds in cases where they are not available, the workarounds result in sub-optimal authoring experience.

If the user uses Babel or TypeScript, these can be covered. Luckily these two combined should cover a pretty decent percentage of all users. For learning / prototyping usage without compile steps, browsers with native support (e.g. Chrome Canary) can also be used.

There is a small risk since these proposals are just stage 3, and are still being actively debated on - technically, there are still chances that they get further revised or even dropped. The good news is that the parts that are relevant here doesn't seem likely to change. There was a somewhat related debate regarding the semantics of class fields being [[Set]] vs [[Define]], and it has been settled as [[Define]] which in my opinion is the preferred semantics for this API.

Decorators

The TypeScript usage relies on decorators. The decorators proposal for JavaScript is still stage 2 and undergoing major revisions - it's also completely different from how it is implemented in TS today (although TS is expected to match the proposal once it is finalized). Its latest form just got rejected from advancing to stage 3 at TC39 due to concerns from JavaScript engine implementors. It is thus still quite risky to design the API around decorators at this point.

Before ES decorators are finalized, we only recommend using decorators in TypeScript.

The decision to go with decorators for props in TypeScript is due to the following:

  1. Decorators is the only option that allows us to express both static and runtime behavior in the same syntax, without the need for double declaration. This is discussed in more details in the Alternatives section.

  2. Both the current TS implementation and the current stage 2 proposal can support the desired usage.

  3. It's also highly likely that the finalized proposal is going to support the usage as well. So even after the proposal finalizes and TS' implementation has been updated to match the proposal, the API can continue to work without syntax changes.

  4. The decorator-based usage is opt-in and built on top of the static props based usage. So even if the proposal changes drastically or gets abandoned we still have something to fallback to.

  5. If users are using TypeScript, they already have decorators available to them via TypeScript's tool chain so unlike vanilla JavaScript there's no need for additional tooling.

this Identity in constructor

In Vue 3 component classes, the this context in all lifecycle hooks and methods are in fact a Proxy to the actual underlying instance. This Proxy is responsible for returning proper values for the data, props and computed properties defined on the current component, and provides runtime warning checks. It is important for performance reasons as it avoids many expensive Object.defineProperty calls when instantiating components.

In practice, your code will work exactly the same - the only cases where you need to pay attention is if you are using this inside the native constructor - this is the only place where Vue cannot swap the identity of this so it will not be equal to the this exposed everywhere else:

let instance

class MyComponent extends Vue {
  constructor() {
    super()
    instance = this // actual instance
  }

  created() {
    console.log(this === instance) // false, `this` here is the Proxy
  }
}

In practice, there shouldn't be cases where you must use the constructor, so the best practice is to simply avoid it and always use component lifecycle hooks.

Two Ways of Doing the Same Thing

This may cause beginners to face a choice early on: to go with the object syntax, or the class syntax?

For users who already have a preference, it is not really an issue. The real issue is that for beginners who are not familiar with classes, the syntax raises the learning barrier. In the long run, as ES classes stabilize and get more widely used, it may eventually become a basic pre-requisite for all JavaScript users, but now is probably not the time yet.

One way to deal with it is providing examples for both syntaxes in the new docs and allow switching between them. This allows users to pick a preferred syntax during the learning process.

Alternatives

Options via Decorator

@Component({
  template: `...`
})
class MyComponent extends Vue {}

This is similar to vue-class-component but it requires decorators - and as mentioned, it is only stage 2 and risky to rely on. We are using decorators for props, but it's primarily for better type-inference and only recommended in TypeScript. For now we should avoid decorators in plain ES as much as possible.

Declaring Prop Types via Generic Arguments

For declaring prop types in TypeScript, we considered avoiding decorators by merging the props interface passed to the class as a generic argument on to the class instance:

interface MyProps {
  msg: string
}

class MyComponent extends Vue<MyProps> {
  created() {
    this.msg // this becomes available
  }
}

However, this creates a mismatch between the typing and the runtime behavior. Because there is no runtime declaration for the msg prop, it will not be exposed on this. To make the types and runtime consistent, we end up with a double-declaration:

interface MyProps {
  msg: string
}

class MyComponent extends Vue<MyProps> {
  static props = ['msg']

  created() {
    this.msg
  }
}

We also considered eliminating the need for double-declaration via tooling - e.g. Vetur can pre-transform the interface into equivalent runtime declaration, or vice-versa, so that only the interface or the static props declaration is needed. However, both have drawbacks:

  • The interface cannot enforce runtime type checking or custom validation;

  • The static props runtime declaration cannot facilitate type inference for advanced type shapes.

Decorators is the only option the can unify both in the same syntax:

class MyComponent extends Vue {
  @prop({
    validator: value => {
      // custom runtime validation logic
    }
  })
  msg: SomeAdvancedType = 'hello'

  created() {
    this.msg
  }
}

Adoption strategy

  • This does not break existing usage, but rather introduces an alternative way of authoring components. TypeScript users, especially those already using vue-class-component should have no issue grasping it. For beginners, we should probably avoid using it as the default syntax in docs, but we should provide the option to switching to it in code examples.

  • For existing users using TypeScript and vue-class-component, a simple migration strategy would be shipping a build of vue-class-component that provides a @Component decorator that simply spreads the options on to the class. Since the required change is pretty mechanical, a code mod can also be provided.

Functional and Async Component API Change

  • Start Date: 2019-03-12
  • Target Major Version: 3.x
  • Reference Issues: N/A
  • Implementation PR: N/A

Summary

  • Functional components must be written as plain functions
    • { functional: true } option removed
    • <template functional> no longer supported
  • Async component must be created via the createAsyncComponent API method

Basic example

import { h } from 'vue'

const FunctionalComp = props => {
  return h('div', `Hello! ${props.name}`)
}
import { createAsyncComponent } from 'vue'

const AsyncComp = createAsyncComponent(() => import('./Foo.vue'))

Motivation

Simplify Functional Components

In 2.x, functional components must be created using the following format:

const FunctionalComp = {
  functional: true,
  render(h) {
    return h('div', `Hello! ${props.name}`)
  }
}

This has the following issues:

  • Even when the component needs nothing but the render function, it still needs to use the object with functional: true.

  • Some options are supported (e.g. props and inject) but others are not (e.g. components). However, users often expect all options to be supported because it looks so similar to a normal stateful component (especially when they use SFC with <template functional>).

Another aspect of the problem is that we've noticed some users are using functional components solely for performance reasons, e.g. in SFCs with <template functional>, and are requesting us to support more stateful component options in functional components. However, I don't think this is something we should invest more time in.

In v3, the performance difference between stateful and functional components has been drastically reduced and will be insignificant in most use cases. As a result there is no longer a strong incentive to use functional components just for performance, which also no longer justifies the maintenance cost of supporting <template functional>. Functional components in v3 should be used primarily for simplicity, not performance.

Detailed Design

In 3.x, we intend to support functional components only as plain functions:

import { h } from 'vue'

const FunctionalComp = (props, slots) => {
  return h('div', `Hello! ${props.name}`)
}
  • The functional option is removed, and object format with { functional: true } is no longer supported.

  • SFCs will no longer support <template functional> - if you need anything more than a function, just use a normal component.

  • The function signature has also changed - h is now imported globally. Instead of a render context, props and slots and other values are passed in. For more details on how the new arguments can replace 2.x functional render context, see the Render Function API Change RFC.

Runtime Props Validation

Props declaration is now optional (only necessary when runtime validation is needed). To add runtime validation or default values, attach props to the function itself:

const FunctionalComp = props => {
  return h('div', `Hello! ${props.name}`)
}

FunctionalComp.props = {
  name: String
}

Async Component Creation

With the functional component change, Vue's runtime won't be able to tell whether a function is being provided as a functional component or an async component factory. So in v3 async components must now be created via a new API method:

import { createAsyncComponent } from 'vue'

const AsyncComp = createAsyncComponent(() => import('./Foo.vue'))

The method also supports advanced options:

const AsyncComp = createAsyncComponent({
  factory: () => import('./Foo.vue'),
  delay: 200,
  timeout: 3000,
  error: ErrorComponent,
  loading: LoadingComponent
})

This will make async component creation a little more verbose, but async component creation is typically a low-frequency use case, and are often grouped in the same file (the routing configuration).

Drawbacks

  • Migration cost

Alternatives

N/A

Adoption strategy

  • For functional components, a compatibility mode can be provided for one-at-a-time migration.

  • For async components, the migration is straightforward and we can emit warnings when function components return Promise instead of VNodes.

  • SFCs using <template functional> should be converted to normal SFCs.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.