Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC Draft: Function-based Component API #35

Closed
yyx990803 opened this issue Jun 6, 2019 · 16 comments
Closed

RFC Draft: Function-based Component API #35

yyx990803 opened this issue Jun 6, 2019 · 16 comments

Comments

@yyx990803
Copy link
Member

yyx990803 commented Jun 6, 2019

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.

@shentao
Copy link
Member

shentao commented Jun 6, 2019

😍

Vue.observable – do we want to use the name observable? As it was already pointed out somewhere else, it might be misleading as it is in 2.6 and might not be a good idea to keep it.

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

onXXX typo

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)

This is huge. What happens to this? Will it be available inside setup? Where should we $emit events? Can this be done inside composition functions by receiving the vm? But this will probably hide the source of the events?

In the compat build, will it be possible to call composition functions in other places than setup?

@Akryum
Copy link
Member

Akryum commented Jun 6, 2019

Where should we $emit events?

I agree this is quite unclear in the current RFC. Maybe something like this:

import { emit } from 'vue'

export function myComposition () {
  emit('change', 42)
}

@posva
Copy link
Member

posva commented Jun 6, 2019

Motivation > Bundle Size

The returned object in setup will still keep the original names, wouldn't it? I was wondering if it's possible to mangle those in production with some activable option, it could reduce the size of apps even more.

Detailed Design > State

is state dropped in favor of always using value. Is was on vuejs/rfcs#22, so I think people will wonder where did it go and mentioning they got merged would help

Regarding the example with props:

setup(props) {
    return {
      msg: `hello ${props.name}!`
    }
  },

Does this return a static value or is it equivalent to creating a computed property

Watchers

If we watch a function that returns an array, does the watcher trigger whenever the array is modified? I'm asking because right now, passing a getter to the watcher will trigger only if the value or reference is modified, which requires the dev to serialize the information to properly trigger one watcher when one of the multiple properties we want to watch changes.

// taken from https://github.com/vuejs/rfcs/pull/22#issuecomment-481084795
watch(
  () => [currentArenaRef.value, currentUserRef.value],
  ([currentArena, currentUser], [prevArena, prevUser]) => {
    findMatches(currentArena,currentUser ) ...
  }
})

can we also do

const currentInfo = observe({
  currentArenaRef,
  currentUserRef
})

watch(
  currentInfo,
  // what happens if the user does
  // () => currentInfo
  // do we warn them?
  // ...
})

const currentArenaAndUser = value([
  currentArenaRef,
  currentUserRef
])

watch(
  currentArenaAndUser,
  // can we do this?
})

Watcher Callback Timing

The sync option triggers the watcher as soon as the value being watched is modified, doesn't it? I think an extra sentence like Doing count.value++ will immediately trigger the watcher
I think it's worth adding that lazy and deep are the same as current API (maybe with a link to docs). What are onTrack and onTrigger?

Adoption Strategy > Options deprecated by this RFC

What is value.raw? is it value

Drawbacks

Another drawback is having pretty much everything inside of one function, leaving to the user how to organize all the logic, which could indeed bring spaghetti code like mentioned in (vuejs/rfcs#22 (comment)). Previously, having methods, data, computed etc, constrained developers to organize their code

@shentao
Copy link
Member

shentao commented Jun 6, 2019

Kinda agree on the drawback @posva mentioned. Thought the setup would be intended to go along the object properties and not entirely replacing them.

@posva
Copy link
Member

posva commented Jun 6, 2019

Thinking about vue router navigation guards. Since properties returned in setup are not accessible through this, we will need to provide a function-based api as well for in-component guards like onBeforeRouteEnter, onBeforeRouteUpdate and onBeforeRouteLeave but at the same time, it's impossible from the router to pick up onBeforeRouteEnter function as they have to be invoked before the component is even instantiated.
Is there a way we make the returned value in setup available in the component so other libraries can still interact with component properties through custom functions

@nekosaur
Copy link
Contributor

nekosaur commented Jun 6, 2019

I am also worried that having everything defined in setup might very well lead to spaghetti code for developers who are unfamiliar with any best practices for using this new API.

With regard to being able to mutate unwrapped values in template, as shown:

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

Would that also apply when passing the unwrapped value through a scoped slot? e.g.

const MyComponent = {
  setup() {
    return {
      count: value(0)
    }
  },
  template: `<div><slot v-bind:count="count"></slot></div>`
}

const OtherComponent = {
  template: `<my-component><button v-slot="props" @click="props.count++">{{ props.count }}</button></my-component>`
}

@shentao
Copy link
Member

shentao commented Jun 6, 2019

What also worries me that this will most likely break plugins like Vuelidate, that rely on component options and this context. I imagine it could be rewritten to become a composition function

const validatedState = vuelidate(state, validationSchema)

I like the idea personally, but I fear this will make the library harder to use, especially since currently it also supports validating computed properties or Vuex getters.

Also, how would we use Vuex with that API? Say using mapState inside setup to get a variable?

const [users, posts] = mapGetters(['users', 'posts'])

@yyx990803
Copy link
Member Author

@shentao Vue.observable – do we want to use the name observable? As it was already pointed out somewhere else, it might be misleading as it is in 2.6 and might not be a good idea to keep it.

Yes, this should be one of the unresolved questions. I was thinking between state and value.raw.

  • Technically state doesn't really exclude primitive values, so the distinction between state and value isn't super clear just by looking at the name.
  • value.raw() makes it more obvious that you are creating a non-wrapped reactive object, but looks... uglier?

@shentao What happens to this? Will it be available inside setup?

this will still be there and will be available in setup.


@nekosaur Would that also apply when passing the unwrapped value through a scoped slot?

No, slot props are always immutable.


@posva I was wondering if it's possible to mangle those in production with some option

This would require template references to be mangled too - which can be very hard.


@posva Does (the props example) return a static value or is it equivalent to creating a computed property

It works just like returning an object from data().


If we watch a function that returns an array, does the watcher trigger whenever the array is modified? I'm asking because right now, passing a getter to the watcher will trigger only if the value or reference is modified, which requires the dev to serialize the information to properly trigger one watcher when one of the multiple properties we want to watch changes.

I don't understand the question. Watchers have always been working this way. If you want to trigger watcher with nested mutations you should be using deep: true.


Spaghetti code

I tend to look at this problem this way: since function-based APIs leave more flexibility to the users, this can lead to both less organized code (by beginners) AND better organized code. However, the less organized code can be largely mitigated with proper documentation of best practices, while the better organized code can never be achieved with current options-based API.

With component options, your code only seem to be organized - in a complex component, logic related to a specific task is often split up between multiple options. Separation of options vs. setup() is like sticking to the separation of HTML/CSS/JS vs. Single File Components. If you put all the logic of your app in a single SFC, that SFC is going to become a monster and become very hard to maintain - so we split the component into many smaller ones. Similarly, if you have a huge setup() function, you split it into multiple functions. Function based API makes better organized code easily possible while with options you are stuck with... options (because splitting into mixins makes things worse).

@shentao
Copy link
Member

shentao commented Jun 6, 2019

With component options, your code only seem to be organized - in a complex component, logic related to a specific task is often split up between multiple options. Separation of options vs. setup() is like sticking to the separation of HTML/CSS/JS vs. Single File Components. If you put all the logic of your app in a single SFC, that SFC is going to become a monster and become very hard to maintain - so we split the component into many smaller ones. Similarly, if you have a huge setup() function, you split it into multiple functions. Function based API makes better organized code easily possible while with options you are stuck with... options (because splitting into mixins makes things worse).

This does work for me. Thank you!
Just one more question and I think I already know the answer, but just need to make sure – there are no hidden restrictions as to what a composition function is returning right? So it can return methods that can be later used inside the component.

I also imagine that possibly, a composition function could return a render function (a functional component) that could then be used inside the components render function? Or template?

@LinusBorg
Copy link
Member

  • Compatibility build: supports both the new function-based APIs AND all the 2.x options.

Does that mean that it's possible to mix the two?

export default {
  setup() {
    return {
      count: value(0)
    }
  },
  computed: {
    plusOne() {
      return this.count + 1
    }
  }
}

Not that I think this is nice, but it would be convenient for migrating bigger components piece by piece from the object notation to the setup function.

@posva
Copy link
Member

posva commented Jun 6, 2019

I don't understand the question. Watchers have always been working this way. If you want to trigger watcher with nested mutations you should be using deep: true.

I'm realizing I have been teaching a way of doing this that is more complicated than it should because using an array also works 😅. It may trigger more times if a value returned in the array is changed multiple times before a flush and is set to the same value it had before being changed the first time

However, the less organized code can be largely mitigated with proper documentation of best practices

I think that practice has shown us that people don't read the documentation, no matter how good it is. Aren't we going to complexify things for people who are starting if they learn first the object based syntax (Vue 2) and then learn the advanced version which allows them to use the smaller version of Vue? It feels like we are making the learning curve steeper.
However, I do agree that having the functional approach is better in bigger projects. So I wonder if having some tool to hint how to convert (I doubt it is possible to fully automatize) plus a good documentation teaching how to move from the non function-based syntax to the function one is enough to keep Vue as approachable as it is today, which is, in my experience the most selling point of Vue

@yyx990803
Copy link
Member Author

I think that practice has shown us that people don't read the documentation, no matter how good it is.

I don't think that's true at all. People probably don't read the API listing in full details, but they definitely read the guide when they first learn Vue. The point is - anyone learning the new API will have to learn it through something - be it docs or 3rd party content - and any 3rd party content will be some sort of spin-off of the docs. So as long as the initial v3 docs introduces it properly, most users will learn to use it properly.

@yyx990803
Copy link
Member Author

yyx990803 commented Jun 7, 2019

@LinusBorg Does that mean that it's possible to mix the two?

Yes. There might be some edge cases (I haven't fully implemented 2.x compat support yet) but theoretically it should work.

@KaelWD
Copy link
Contributor

KaelWD commented Jun 7, 2019

const plusOne = computed(() => count.value + 1)

Could that also be const plusOne = computed(() => this.count + 1) or is setup more like beforeCreate?

@yyx990803
Copy link
Member Author

@KaelWD it's more like beforeCreate. It will be called only after beforeCreate and props resolution, but before all other 2.x options.

@gustojs
Copy link
Member

gustojs commented Jun 7, 2019

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

I like how it's much easier to switch between a read-only and writeable computed properties without having to rewrite between an object and functions.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants