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

Function-based Component API #42

Closed
wants to merge 32 commits into from

Conversation

@yyx990803
Copy link
Member

yyx990803 commented Jun 8, 2019

A proposal that consolidates upon #22 (Advanced Reactivity API), #23 (Dynamic Lifecycle Injection) and the discontinuation of #17 (Class API).

Full Rendered Proposal

High-level Q&A

Is this like Python 3 / Angular 2 / Do I have to rewrite all my code?

No. The new API is 100% compatible with current syntax and purely additive. All new additions are contained within the new setup() function. Nothing is being removed or deprecated (in this RFC), nor do we have plan to remove / deprecate anything in the foreseeable future. (A previous draft of this RFC indicated that there is the possibility of deprecating a number of 2.x options in a future major release, which has been redacted based on user feedback.)

Details

Is this set in stone?

No. This is an RFC (Request for Comments) - as long as this pull request is still open, this is just a proposal for soliciting feedback. We encourage you to voice your opinion, but please actually read the RFC itself before commenting, as the information you got from a random Reddit/HN thread can be incomplete, outdated or outright misleading.

Vue is all about simplicity and this RFC is not.

RFCs are written for implementors and advanced users who are aware of the internal design constraints of the framework. It focuses on the technical details, and has to be extremely thorough and cover all possible edge cases, which is why it may seem complex at first glance.

We will provide tutorials targeting normal users which will be much easier to follow along with. In the meanwhile, check out some examples to see if the new API really makes things more complex.

I don't see what problems this proposal solves.

Please read this reply.

This will lead to spaghetti code and is much harder to read.

Please read this section and this reply.

The Class API is much better!

We respectfully disagree.

This RFC also provides strictly superior logic composition and better type inference than the Class API. As it stands, the only "advantage" the Class API has is familiarity - and we don't believe it's enough to outweigh the benefits this RFC provides over it.

This looks like React, why don't I just use React?

First, the template syntax doesn't change, and you are not forced to use this API for your <script> sections at all.

Second, if you use React, you'll most likely be using React Hooks. This API is certainly inspired by React hooks, but it works fundamentally differently and is rooted in Vue's very own reactivity system. In addition, we believe this API addresses a number of important usability issues in React Hooks. If you cannot put up with this API, you will most likely dislike React Hooks even more.

file-exploerer-before

file-exploerer-compare

@yyx990803 yyx990803 added core 3.x labels Jun 8, 2019
@danielelkington

This comment has been minimized.

Copy link

danielelkington commented Jun 8, 2019

While I was initially disappointed about the class API being dropped, I’m now convinced that it was the right decision - this proposal is far superior. I can see how this will help to more easily break up component logic in a very typescript friendly manner with a clean and beautiful API, while avoiding annoying caveats that exist with React Hooks. Will solve so many issues we’ve had with “monster” components and the difficulty of sharing stateful logic. Bravo Evan and team.

Edit: I've written up some thoughts expanding on why I think the new syntax is great.

@posva

This comment has been minimized.

Copy link
Member

posva commented Jun 8, 2019

Like data, setup would make more sense as a sync function, we cannot just block the rendering like that. The loading problem you are talking about doesn't need anything new on Vue side, it can already by dealt with the async factory or userland solutions like vue-promised

@beeplin

This comment has been minimized.

Copy link

beeplin commented Jun 8, 2019

What about therender() function?

In #17 (comment) the render function receives state from the setup function:

 render({ state, props, slots }) {
    // `this` points to the render context and works same as before (exposes everything)
    // `state` exposes bindings returned from `setup()` (with value wrappers unwrapped)
  }

While in the old https://github.com/vuejs/rfcs/blob/render-fn-api-change/active-rfcs/0000-render-function-api-change.md the render function receives just (props, slots, attrs, vnode):

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

  }

Which will be the final design?

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

This comment has been minimized.

Copy link
@leopiccionia

leopiccionia Jun 8, 2019

Is the renaming from destroy to unmount verb intentional?

Or is the mount/unmount pair analogous to create/destroy pair?

This comment has been minimized.

Copy link
@yyx990803

yyx990803 Jun 9, 2019

Author Member

Yes, destroy -> unmount is intentional.

This comment has been minimized.

Copy link
@Nandiin

Nandiin Jun 18, 2019

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

Since there will be a renaming, I'd recommend listing all available lifecycle hook functions in the proposal explicitly.

@beeplin

This comment has been minimized.

Copy link

beeplin commented Jun 9, 2019

Is provide/inject still necessary in the new api? IMO they were designed for sharing states between ancestors/offsprings, like a limited version of vuex, but now with the value function, we can easily achieve inter-component data sharing by creatinga reactive variable and importing it into different components, which makes inject/provide unnecessary.

What's more, in the old inject/provide pattern, it was always difficult to track where an injected state comes from when reading codes of child components. Just like the drawbacks of mixin, it forces us to go through multiple components/files to understand the logic of one single components.

@skyrpex

This comment has been minimized.

Copy link

skyrpex commented Jun 9, 2019

Is provide/inject still necessary in the new api? IMO they were designed for sharing states between ancestors/offsprings, like a limited version of vuex, but now with the value function, we can easily achieve inter-component data sharing by creatinga reactive variable and importing it into different components, which makes inject/provide unnecessary.

What's more, in the old inject/provide pattern, it was always difficult to track where an injected state comes from when reading codes of child components. Just like the drawbacks of mixin, it forces us to go through multiple components/files to understand the logic of one single components.

I think provide/inject is still necessary: the value you want to inject is not living in a JS module but in an existing component somewhere in the render tree. You could still import a symbol from the component that provides the values.

This was referenced Jun 9, 2019
@yyx990803

This comment was marked as outdated.

Copy link
Member Author

yyx990803 commented Jun 9, 2019

@beeplin render({ state, props, slots }) will likely be the final design. As it turns out we have more things we'd like to expose to the render function.

As for provide/inject, it is still important because it allows a component to inject values from the context it is being used in, instead of hard-wiring it to a singleton. For example in SSR, it is important to be able to inject a fresh store instance for each request - which you cannot do if you directly import a store instance in your component.

@beeplin

This comment has been minimized.

Copy link

beeplin commented Jun 9, 2019

On type inference: how to share type information among props, setup(props) and render({props, state, slots})?

React hooks, despite its all weirdness and performance penalties, helps put all component logics in one single function and are therefore naturally type-friendly. Our function-based API, on the other hand, divides component logics into two functions, setup and render, and at least one more config object, props. So how to let setup know type information of props, and render know that of props and states (and even more, slots, since slots seems to be defined externally)? Will vetur do this automatic type mapping?

@beeplin

This comment has been minimized.

Copy link

beeplin commented Jun 9, 2019

Or perhaps we need a wrapper like const Component = createComponent({ props, setup, render}) to help type inference?

@HerringtonDarkholme

This comment has been minimized.

Copy link
Member

HerringtonDarkholme commented Jun 9, 2019

@beeplin

On type inference: how to share type information among props, setup(props) and render({props, state, slots})?

We do need a wrapper function playground link

Some typescript-foo can bring us full type safety.

@Aferz

This comment has been minimized.

Copy link

Aferz commented Jun 9, 2019

This RFC looks really promising. Thank you Vue team for struggling so hard to achieve a monsterkill design. It looks really polished and clean. Can't wait to play with it!

@smolinari

This comment has been minimized.

Copy link

smolinari commented Jun 9, 2019

This is so awesome and I wouldn't have known how awesome 3.0's new API would be, if it weren't for this great write-up (it reads like future documentation), which puts the other RFCs into a much better context Evan. Great job! I love the fact you are sticking to JavaScript's roots and still accommodating the TS crowd (to a point). That is a tight-rope act and you are handling it amazingly well. It shows your not just a great developer, but a great leader too! 🥇 💯 👍 💟

Scott

@yyx990803

This comment has been minimized.

Copy link
Member Author

yyx990803 commented Jun 9, 2019

@beeplin yes, you need to put the object in a function to get inferred typing for arguments passed to setup() and render() (same as with 2.x you need to wrap the object in Vue.extend). In SFCs, Vetur can do that for you implicitly.

@brennj

This comment has been minimized.

Copy link

brennj commented Jun 9, 2019

Love this! I cannot wait for it to be available to play around with, thank you and the Vue team for all your hard work on these RFCs.

I do have a question on inject and provide. I know in the current Vue documentation there is a warning against using it. Unless it is of more advanced cases like for plugin development. Will this still be the case? Or is it now more like using modern the Context API in React? Where you use it over Redux/Vuex for some smaller global state concerns like theming?

I ask as I found it confusing using Strings as provider keys. There is the potential problem of conflicting identifiers available for injection. Seeing this Symbol example makes it a lot more clearer. :) Is it worth only allowing passing Symbols? (If out of scope of this discussion, feel free to gloss over this!)

Again, amazing work and can't wait!

@sqal

This comment has been minimized.

Copy link

sqal commented Jun 9, 2019

@HerringtonDarkholme Regarding TypeScript. Will there be a way to define props interface (like in the example below), because the one thing I've always missed in vue is the ability to define the shape of an object prop..

interface UserCardProps {
  user: {
    firstName: string;
    lastName: string;
    email: string;
  }
}

const UserCard = createComponent<UserCardProps>({
  props: {
    user: Object
  }
})

Also what about case when prop has a default value:

props: {
  count: {
    type: Number,
    default: 0
  }
}

Your example seems broken currently

@HerringtonDarkholme

This comment has been minimized.

Copy link
Member

HerringtonDarkholme commented Jun 9, 2019

In current watch function seems always require its first argument as "dependency source". It does provide a way for users to track old value and new value, but I think a watcher without explicit dependency is also useful (like autorun from mobx).

Consider a list component which fetches a user's articles. We probably will write some code like

setup(prop) {
  const page = value(0)
  const articles = value([])
  watch([() => prop.id, page], async () => {
     articles.value = await fetchArticles(prop.id, {page: page.value})
  })
}

The example code now is simple. But it already duplicates logic: the dependency array reflects reactive variables used in watch handler. And later we might add more variables like filter or sort, making dependency array unwieldy. Also, data fetching usually won't need to know previous value.

Since we have already tracked reactive variable usage. It is nice to have a watcher sensitive to implicit dependencies.

@tochoromero

This comment has been minimized.

Copy link

tochoromero commented Jun 9, 2019

I like the new API, I'm excited about it, but I'm worried about the depreciation of some of the 2.x props.

The new API, though extremely powerful, it has a steeper learning curve than 2.x. I believe beginners will have a harder time getting started, robbing Vue of one of his bigger strengths.

Wouldn't it be better to keep all of the 2.x options and opt-in to have them removed if you choose to?
Newcomers can write components the easier 2.x
way and slowly migrate to the new 3.x way.

And yes, I'm aware the 2.x will still be around on a compatibility mode, but it will eventually go away and my argument will hold then.

@LinusBorg

This comment has been minimized.

Copy link
Member

LinusBorg commented Jun 9, 2019

@tochomero I think you misunderstand.

There will be a compatibility build of Vue 3, which will contain all of the deprecated features.

You can optionaly use the smaller build where these features are already removed.

So users do have the choice that you are asking for, as far as I can see.

@tochoromero

This comment has been minimized.

Copy link

tochoromero commented Jun 9, 2019

@LinusBorg but eventually the deprecated properties will go away, that is what deprecated means, doesn't it?

I guess what I'm asking is to keep them around for the foreseeable future. And maybe that is the plan already, I was just tipped off by the deprecated label.

@skyrpex

This comment has been minimized.

Copy link

skyrpex commented Jul 24, 2019

Will we be able to inspect components created by the Function-based Component API using the Vue Devtools? I specifically ask about the component properties (state, computed, etc).

@beeplin

This comment has been minimized.

Copy link

beeplin commented Jul 24, 2019

@skyrpex Yes as long as you expose them by returning them in setup() { ... return {xxx}}, xxx can be seen in dev tools.

@skyrpex

This comment has been minimized.

Copy link

skyrpex commented Jul 26, 2019

Ah yes, of course. Not sure what I was thinking 🤔

@milky2028

This comment has been minimized.

Copy link

milky2028 commented Jul 30, 2019

@Akryum I know people were quite upset about the whole lean/standard build thing, but I think there are a few lingering questions related to the bundle.

If we convert all our components to the composable function API, will there be some sort of shrink in bundle size? What if we use none of the composable functions, will they be tree-shaken out? I'm curious about how these new changes will affect the final size of Vue itself. If this info isn't clear yet, I understand.

All in all, great work everyone! I'm excited to start using the new API.

@lowski lowski referenced this pull request Aug 5, 2019
@csmikle

This comment has been minimized.

Copy link

csmikle commented Aug 6, 2019

I strongly want to avoid having value() and .value all throughout my code, as I like to have functions/classes in files that can be dropped into another javascript project and reused. I'd like to be able to wrap them to vue-ify them, and drop them into the setup function.
So just so I'm clear then:
If I prefer using obj.count over count.value and so use the state/observable function, there is no difference in result? The two work the same everywhere?

So I can do: { count, name } = state({ count: 0, name: { first: 'Bob'', last: 'Jones'} })
and access 'count' throughout the function like just count++ and num = count ? And similarly name.first = 'Mike' , and it'll all be reactive?

Sorry, I'm no expert, haven't used the observable API. I've used the class component plugin with this kind of plug and play, and I think this RFC looks very good for similar benefits except for the dependence on importing 'value from vue' into lots of different places.

@skyrpex

This comment has been minimized.

Copy link

skyrpex commented Aug 6, 2019

So I can do: { count, name } = state({ count: 0, name: { first: 'Bob'', last: 'Jones'} })

I think that name.first = 'Mike' would work, but count++ wouldn't. Destructuring primitive data like numbers will break reactivity. You'll need to do:

const data = state({ count: 0, name: { first: 'Bob'', last: 'Jones'} });
data.count++; // works
data.name.first = 'Mike'; // works as well.

Which isn't that bad :)

@skyrpex

This comment has been minimized.

Copy link

skyrpex commented Aug 6, 2019

If we convert all our components to the composable function API, will there be some sort of shrink in bundle size?

@milky2028 I'm curious about the exact numbers as well, but definitely there'll be some sort of shrink in bundle size. All the variable names that you destructure (const { myVar } = otherVar) can be mangled and minified. The more you used these variables in your code, the more bytes you'll save. This can't be done with variables that are accessed through an object, like prototype methods.

For example, the following code:

import { value, computed } from "vue";

export default {
    setup() {
        const firstName = value("");
        const lastName = value("");
        const fullName = computed(() => `${firstName.value} ${lastName.value}`);
        return {
            fullName,
        };
    },
};

Can be mangled into this:

const b = require("vue"), v = b.value, c = b.computed;

module.exports = {
    setup() {
        const f = v("");
        const l = v("");
        const n = c(() => `${f.value} ${l.value}`);
        return {
            fullName: n,
        };
    },
};
@donnysim

This comment has been minimized.

Copy link

donnysim commented Aug 6, 2019

I just wonder why value? Why choose one of the more commonly used variable name by developers?

@skyrpex

This comment has been minimized.

Copy link

skyrpex commented Aug 6, 2019

I just wonder why value? Why choose one of the more commonly used variable name by developers?

It's short, it has meaning (create a value), and it's generic.

I personally don't think value is really common in code. You can choose a better name for your variables unless you're doing very generic programming. In that case, you have different ways to overcome that.

@milky2028

This comment has been minimized.

Copy link

milky2028 commented Aug 6, 2019

@skyrpex Ahhh, this makes sense. I know a big reason React was pushing for hooks is because class methods can't be minified. I didn't realize the same applied for class properties.

Thanks!

@csmikle

This comment has been minimized.

Copy link

csmikle commented Aug 6, 2019

I think that name.first = 'Mike' would work, but count++ wouldn't. Destructuring primitive data like numbers will break reactivity. You'll need to do:
Which isn't that bad :)

Thank you, helpful. Not bad at all, that works. I'll definitely use data.count over count.value most of the time. I guess it wasn't mentioned a lot in this writeup becaus ethe observable api is old news to you guys and the RFC is about the new setup function.

When I install the plugin for this functional API RFC, do I get the rename to state, or do I continue using observable until 3.0 comes out?

I just wonder why value? Why choose one of the more commonly used variable name by developers?

It's short, it has meaning (create a value), and it's generic.

One thing I noticed in this writeup is I was getting confused by the 'value' in the watch method, until I re-read and saw that it was a reference to the output of the first parameter. The name was definitely peppered in examples throughout, mixed with the wrapper method being discussed.

@smolinari

This comment has been minimized.

Copy link

smolinari commented Aug 7, 2019

When I install the plugin for this functional API RFC, do I get the rename to state, or do I continue using observable until 3.0 comes out?

The plugin uses this RFC's original naming conventions. So value for making primitives reactive and state for objects.

Scott

@TerenceZ

This comment has been minimized.

Copy link

TerenceZ commented Aug 13, 2019

Recently I wrap vue2 to vue3's function APIs, and write some applications. I realize that if one can provide ref hook, and make setup to return exposed props / methods with render method (otherwise, we have to write createComponent<Props>({ setup() {}, render(props) {}), and rerturn all states used in render from setup), we can make writing components (with TypeScript) easier. Here's an example:

// A has type of Component<PropsA, { inc(a: number): void; count: number; }>
const A = createComponent((props: PropsA) => {
  const count = value(0)
  const inc = (a: number) => {  stateA.value += a  }

  return {
    inc,
    count,
    // render is reserved to render
    render(props) { // props' type can be inferred from PropsA
      return <div>{props.start + count.value}</div>
    },
  }
})

const B = createComponent(() => {
  const root = ref<HTMLElement>()
  const a = ref<typeof A>() // can be inferred a's exposed props and methods from A.

  onMounted(() => {
    a.value.inc(10)
  })

  // we can use `ref={a}` to inject A's instance to a.
  return () => (
    <div ref={root}><A ref={a} start={0} /></div>
  )
})
@nekosaur

This comment has been minimized.

Copy link

nekosaur commented Aug 13, 2019

The current proposal does not show how to handle default values for injections. I would assume that it would just be the second argument to inject

const Consumer = createComponent({
  setup() {
    const count = inject(CountSymbol, 42) // count's type is Value<number>
    console.log(count.value) // 42 if no provide is found
    return {
      count
    }
  }
})

But for clarity's sake it should probably be mentioned in the proposal?

@backbone87

This comment has been minimized.

Copy link

backbone87 commented Aug 13, 2019

@TerenceZ i like this idea. only one thing: i would use a symbol for the render method, just to leave the namespace untouched

@jasonbodily

This comment has been minimized.

Copy link

jasonbodily commented Aug 16, 2019

Intelligibility and simplicity are my favorite part of Vue, and it's how I sold my team on Vue 3 years ago when the majority weren't initially on board. This RFC introduces a few concerns for me:

  • Codebases are harder to read when there are several layers of function syntax () => { () => {} }, which seems to be perpetuated with the new setup syntax.
  • Most examples posted here are longer and less readable. (e.g., @Akryum's posted: https://suspicious-mclean-0e54c3.netlify.com/). The solutions are often 15-25% more code (by character count). If the source code has to be larger and less readable so that the library can be a few kb smaller, that seems backwards.
  • More logic is written in imported wrappers. setup pushes all your code one more tab to the right and dumps a lot of code into ({ here }). It's silly, I know, but every level introduces another layer of complexity in a developer's mind. @lloydjatkinson concerns are valid because they highlight what appealed to users looking to adopt simple frameworks in the first place. I was very skeptical to read someone report their new developers thought setup was easier and more intuitive. Having taught a handful of developers myself, embedded code and functions in functions leave students staring.
  • .value. I personally would never use it. That's another layer of complexity that splits the mind and introduces complexity the developer ideally shouldn't have to be thinking about. I'll take the state way of doing things, but there's another dependency and function invocation.
  • I haven't run across a lot of the problems people are citing as reasons for this change, so I don't feel the pain (and I've written my share of mixins, directives, filters, and external services). I don't use Typescript (yes I've coded in many strongly typed languages and am aware of the benefits) so it's curious to be reading how much of all this is for the sake of that community.

Having said this, I'm not opposed to this RFC, and I think there's plenty of room for improvement in Vue. My hope is that some of the simplicity and readability could be restored. (Yes, I know some feel the new way is neater, but I am politely disagreeing for the reasons above which I think are objective enough) Simplicity is everything and it can't be a 2nd class citizen. It's the first thing anyone notices is missing when choosing a framework.

@yyx990803

This comment has been minimized.

Copy link
Member Author

yyx990803 commented Aug 17, 2019

Thanks everyone for your feedback here! This RFC has gone through significant iteration, including an important change in direction regarding the purpose of the proposed API and its role in the Vue ecosystem. Most notably, it originally mentioned possible deprecations which are no longer being considered, based on your feedback.

Due to all these changes, navigating the discussion has become difficult and confusing, particularly for newcomers. To make it easier, we're closing this PR and migrating to an updated RFC that includes all the latest technical changes, and also better communicates the updated role we now expect the API to have in most users' day-to-day work. Please continue your feedback and questions there. 🙂

@yyx990803 yyx990803 closed this Aug 17, 2019
@builder7777

This comment was marked as off-topic.

Copy link

builder7777 commented Sep 27, 2019

what happened to time slicing?

@Codermar

This comment has been minimized.

Copy link

Codermar commented Nov 9, 2019

The composition API is a really awesome thing 😉 It takes a bit of getting used to but, it makes so much sense it’s almost strange you guys did not come up with this before :-)

mesqueeb added a commit to mesqueeb/rfcs that referenced this pull request Nov 17, 2019
it was still referencing vuejs#42 as opposed to vuejs#78
@turbobuilt

This comment has been minimized.

Copy link

turbobuilt commented Nov 18, 2019

It's an interesting api. I just don't like having to export all the variables you declared at the end of setup(). This is a second step that will be the source of annoying bugs - forgetting to export variables. I think it could be done without having to export the variables at the end. As it stands you are kind of having to define the variable twice - which is going to be a source of bugs and frustration.

I don't exactly have a solution for it, but as a user the idea of defining a variable and then having to export it somewhere else seems like housekeeping that will slow me down and introduce bugs as I add and remove variables.

In other words, I think we should figure out a way to be DRY.

@builder7777

This comment has been minimized.

Copy link

builder7777 commented Nov 18, 2019

What's wrong with vue-class-component decorator? It allows grouping data with their respective methods in logical sections, it also enforces types, and it avoids the issue @turbobuilt mentioned regarding forgetting to export variables.

@yyx990803

This comment has been minimized.

Copy link
Member Author

yyx990803 commented Nov 18, 2019

PSA: this is an outdated thread.

  • The latest RFC has already been merged and a deployed version can be read here

  • It is still open to discussion but please use the new thread here

@vuejs vuejs locked as resolved and limited conversation to collaborators Nov 18, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
You can’t perform that action at this time.