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

Open
wants to merge 31 commits into
base: master
from

Conversation

@yyx990803
Copy link
Member

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.

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

@danielelkington

This comment has been minimized.

Copy link

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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.

@tangst

tangst approved these changes Jun 26, 2019

Copy link

left a comment

Thanks for amending the code example to show how 2.0 syntax would work alongside 3.0 syntax.

@tigregalis

This comment has been minimized.

Copy link

commented Jun 26, 2019

However, I haven't seen a single example that shows how this would help us breakup the logic from the component script code. All of the examples still show all of the code under <script>, but now sitting inside a setup() method, so we have more nesting and complexity when trying to read through someone's code. There are also examples showing methods above the export for the component that just seems to add even more noise. I was hoping to see an example where the code for multiple computed properties would exist in a single computed.js file that we would import into our component and insert into the return for setup(). Unfortunately it seems that if the computed property relies on a prop or data value, then I wouldn't be able separate that code into a module?

Example

my-component/computed.js

import { computed } from 'vue';

export default {
  // How can I pull in "firstName" and "lastName" from setup() props ?
  firstName: computed(() => `${firstName} ${lastName}`),
};

my-component/my-component.vue

<template>
  Hello {{ fullName }}
</template>

<script>
import { fullName } from './computed';

const MyComponent = {
  setup(props) {
    return {
      fullName, // How do I send props to the computed function?
    };    
  }
}
</script>

@ejwaibel I've modified your example below:
Edit: Now with both files. Haha.

my-component/computed.js

import { computed } from 'vue';

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

my-component/my-component.vue

<template>
  Hello {{ fullName }}
</template>

<script>
import { fullName } from './computed';

const MyComponent = {
  setup(props) {
    return {
      ...fullName(props),
      // since`fullName` is a function that returns computed
      // values, functions, etc; you can just pass the props in.
      // (`setup` is a function that accepts props as a first parameter)
    };    
  }
}
</script>

It seems like the point of setup's grouping is to extrapolate related computed properties, watchers, methods, etc... so I don't really know why we'd move a computed property alone to another file, but... seems like you can do it without issue based on the current documentation.

@ejwaibel @aminimalanimal
I think the idiomatic way to do it would probably be along the following lines. I think a slightly more complex example better demonstrates what this RFC is intended to solve or make easier, so I've also expanded the example to also demonstrate:

  1. reusing the same logic from a library (useNaming() called twice, with different arguments, props and me)
  2. exporting multiple variables from a single library function ({ fullName, initials })
  3. aliasing (fullName => yourFullName and myFullName)
  4. local state (me)

lib/naming.js

import { computed } from 'vue';

export default function (data) {
    const fullName = computed(() =`${data.firstName} ${data.lastName}`);
    const initials = computed(() =`${data.firstName.substr(0,1)}.${data.lastName.substr(0,1)}.`);
    return { fullName, initials };
}

components/greeting-component.vue

<template>
  Hello {{ yourFullName }} ({{ yourInitials }}), my name is {{ myFullName }} ({{ myInitials }}).
</template>

<script>
import { state as reactive } from 'vue';
import { useNaming } from '../lib/naming';

export default {
  props: {
    firstName: String,
    lastName: String
  },
  setup(props) {
    // local state
    const me = reactive({
      firstName: 'John',
      lastName: 'Smith'
    });

    // using props
    const { fullName: yourFullName, initials: yourInitials } = useNaming(props);

    // using local state
    const { fullName: myFullName, initials: myInitials } = useNaming(me);
    return {
      yourFullName,
      yourInitials,
      myFullName,
      myInitials
    };    
  }
}
</script>

components/parent-component.vue

<template>
  <GreetingComponent first-name="Jane" last-name="Doe">
</template>

<script>
import GreetingComponent from './greeting-component.vue';

export default {
  components: {
    GreetingComponent
  }
}
</script>

Prints: Hello Jane Doe (J.D.), my name is John Smith (J.S.).

@tigregalis

This comment has been minimized.

Copy link

commented Jun 26, 2019

@nijikokun

The more I think about this API, the more I foresee multiple issues arising within my organization. While I am overall personally very excited about it.

My organization uses Vue because it comes with out-of-the-box structure, great for teams, less cognitive overload and bike-shedding.

This API proposal loosens that contract, concept, aspect... whatever word you prefer.

To the argument of: "That's a documentation issue".

Perhaps, however, even with the best practices examples... I've already even started denoting that you could have a Class structure with the new API, and given my personal opinion on how it could look and that is the fear with this proposal.It's not an opinionated way of structure, it's becoming very personal.

Even talking with the teams, they've already started discussing this aspect.

I think that Vue needs to not be too opinionated in terms of the API to allow that flexibility for different use cases and workflows, but still have a canonical "Vue way" of doing things. That's why, beyond the introductory Vue Guide, there's the Vue Style Guide, with associated tooling to enforce it. Going even further is Chris Fritz's Vue Enterprise Boilerplate, "an ever-evolving, very opinionated architecture and dev environment for new Vue SPA projects using Vue CLI 3".

@TerenceZ

This comment has been minimized.

Copy link

commented Jun 26, 2019

The function based component is nice, and I have some suggestions:

  • Able to register cleanup callback in some lifecycle hooks (e.g., onMounted()).

    Because for some lifecycle hooks(e.g., onMounted()), the cleanup is often required. It's more compact
    for related logic.

// The 1st proposal.
onMounted(() => {
  const update = e => {}
  window.addEventListener('mousemove', update)
  return () => window.removeEventListener('mousemove', update)
})

// The 2nd proposal.
// Just like the watch hook, it pass optional onUnmounted().
onMounted((onUnmounted) => {
  const update = e => {}
  window.addEventListener('mousemove', update)
  onUnMounted(() => window.removeEventListener('mousemove', update))
})
  • Is it possible to provide something like createRef() to ref component / element?
    Because it's more friendly to use with typescript (e.g., createRef<SomeComponent>() vs setup(props, { refs })).
const Comp = createComponent(() => {
    const componentARef = createRef<ComponentA>()
    // ...

    return () => h('div', {
      onClick(e) {
        componentARef.value.click(e)
      },
    }, [h(ComponentA, { ref: componentARef })])
})
@yyx990803

This comment has been minimized.

Copy link
Member Author

commented Jun 26, 2019

@TerenceZ

  • re lifecycle cleanup: the 1st proposal has the same problem with watcher callback: the return value of an async function is needed for proper error handling. The 2nd proposal can be considered.

  • re refs: you can just use value() as a generic ref container.

@jacekkarczmarczyk

This comment has been minimized.

Copy link

commented Jun 26, 2019

@yyx990803 does setup function needs to be fully synchronous? I think the RFC should mention this, in both cases - when it must be sync and when can be async (and what are the restrictions). In 2.0 it's quite common question how to deal with async data during initialization. (sorry if RFC already mentions it and I just missed it)

I mean code like:

async setup () {
  const data = await getData();
  const computed = (() => data.foo);
  onUnmounted(() => {
    doSomething();
  });
  return { ... }
}
@ais-one

This comment has been minimized.

Copy link

commented Jun 26, 2019

@jacekkarczmarczyk

This comment has been minimized.

Copy link

commented Jun 26, 2019

I'm using (await are in fact promises). That doesn't change anything in my question

@nijikokun

This comment has been minimized.

Copy link

commented Jun 26, 2019

@tigregalis I'm not sure I agree with that. But, I'd love to sit down and have an honest chat with @yyx990803 about large organization adoption of Vue and how heavily these changes will impact them, and the foreseeable outcomes of that.

I'm empathetic towards Evan, as a developer I can see the benefits and I enjoy them (from a purely programming / development toy), as someone with a business hat, product hat, and manager hat... I can easily see the response to this even with the whole "it's additive" asterisk. Engineers on teams say this all the time. In large organizations that is a huge red flag.

@mika76

This comment was marked as off-topic.

Copy link

commented Jun 26, 2019

@nijikokun he's made himself quite clear on Twitter how he feels about corporate adoption.

https://twitter.com/youyuxi/status/1142447919879999488?s=21

@enekesabel

This comment has been minimized.

Copy link

commented Jun 26, 2019

After rolling around in my bed for hours, I might have come up with a solution that provides the reusability of the functional api, while keeping the object-based declaration.
It introduces 3 new things:

  • a global use function which makes composing components possible. It requires 2 inputs: A Vue component definition, and the props to pass. It would return the exposed values of the composed component in an object.
  • a compose method - similar to setup(). It reuses logic from other components using use, and returns an object whose keys would be available on 'this'.
  • an expose method which declares which properties should be exposed by the component if it's used for composition by calling use.

I think this has all the benefits of the functional api, but keeps the syntax on our beloved standard way. Ofc I'm posting this to prove me wrong :D

Here's my example:

<template>
    <div>
        <div>Last recorded position: {{ x }}, {{ y }}</div>
        <div>Distance from top-left corner: {{distance}}</div>
        <button @click="toggleRecording">{{buttonText}}</button>
    </div>
</template>

<script>
  import {use} from 'vue';

  // object-based, but reusable component definition
  const MouseTracker = {
    props: {
      active: { //tracking can be turned on/off
        type: Boolean,
        required: true,
      },
    },
    // could be used to provide a clear interface
    // exposed variables would be available when using 'use'
    // maybe could be all of the data, computed and methods by default
    // in this case we don't expose start & stop, so extended components
    // can only start/stop using the prop
    expose: () => ({
      x: this.x,
      y: this.y,
    }),
    data: () => ({
      x: 0,
      y: 0,
    }),
    beforeDestroy() {
      this.stopTracking();
    },
    methods: {
      startTracking() {
        window.addEventListener('mousemove', this.updateMouse);
      },
      stopTracking() {
        window.removeEventListener('mousemove', this.updateMouse);
      },
      updateMouse(e) {
        this.x = e.pageX;
        this.y = e.pageY;
      },
    },
    watch: {
      active: {
        immediate: true,
        handler(isActive) {
          if (isActive) {
            this.startTracking();
          } else {
            this.stopTracking();
          }
        }
      }
    }
  };

  // the component to be extended with the functionality
  export default {
    compose() {
      // reusing component, providing props
      const {x, y} = use(MouseTracker, {active: this.recordingActive});

      // returned keys would be available on 'this'
      // so the extended component can use them
      return {
        x,
        y,
      };
    },
    data: () => ({
      recordingActive: false,
    }),
    computed: {
      buttonText() {
        return this.recordingActive ? 'Stop' : 'Start';
      },
      distance() {
        // reusing exposed variables from MouseTracker
        return Math.sqrt(Math.pow(this.x, 2) + Math.pow(this.y, 2))
      }
    },
    methods: {
      toggleRecording() {
        this.recordingActive = !this.recordingActive;
      }
    }
  }

</script>
@nijikokun

This comment has been minimized.

Copy link

commented Jun 26, 2019

@mika76 I agree with him. What I am talking about in my post is very different. If you feel it is not I am open to hearing your opinion. My offer still stands, regardless.

@enekesabel I too came to a similar structure. I went on a few variations of what you've devised, one of which was retaining the original structure but converting the lifecycle methods to arrays. Which components then decompose into. The reason for them being arrays is to retain declaration order. During the lifecycle event, each method is invoked, in the order defined.

Which led me to the ultimate conclusion that while I think the API proposal is very good for personal projects, I can foresee this lack of opinion leading to high code fragmentation, which unfortunately will be a point brought up when trying to bring teams into using Vue in large organizations.

@yyx990803

This comment has been minimized.

Copy link
Member Author

commented Jun 26, 2019

@nijikokun

  • It's been made clear that we no longer have any plan to deprecate the object-based API
  • you won't have to rewrite any code because of this RFC, ever (this excludes breaking changes introduced by other RFCs)
  • Large organizations are the very audience that will benefit the most from this RFC (in the long run).
    • Code using 2.x API can be refactored into function-based code piecemeal, even inside a single component:

      export default {
        // replace data() with setup()
        setup() {
      +   const { c1, m1, baz } = useFeature()
          return {
            foo: 1,
            bar: 2,
      -     baz: 3
      +     baz,
      +     c1,
      +     m1
          }
        },
        computed: {
      -   c1() { /* ... */ },
          // other computed
        },
        methods: {
      -   m1() { /* ... */ },
          // other methods
        }
      }

      Even for code staying on the object based API, the function API can serve as a much more flexible replacement for mixins.

    • The organizational benefits of the function-based API (organizing code by topic vs. by type) requires a certain level of initial investment (settling down on and enforcing a set of best practices in the team), but it will significantly increase maintainability of large projects, saving cost in the long run.

@yyx990803

This comment has been minimized.

Copy link
Member Author

commented Jun 26, 2019

@mika76 this is not the first time you have been making personal pokes at me. This kind of behavior does not help anyone, please stop it. If you continue I will have to block you.

@skyrpex

This comment has been minimized.

Copy link

commented Jun 26, 2019

I think this has all the benefits of the functional api

@enekesabel It doesn't. It's almost the same as the setup method, but missing tree-shakeability and type inference, and adding more concepts (the expose method and the use method).

@nijikokun

This comment has been minimized.

Copy link

commented Jun 26, 2019

The organizational benefits of the function-based API requires a certain level of initial investment (settling down on and enforcing a set of best practices in the team), but it will significantly increase maintainability of large projects, saving cost in the long run.

@yyx990803 Thanks for responding. I assumed that was the case, however, I do believe that this point hits the nail on the head, and is where I have reservations with agreeing because there were two explicit reasons for adopting Vue, reactivity out of the box, and the best practices / structure it defined. Note, this isn't saying that this can't be done for the third version, there are just competing ways and a decision / transitory phase must occur. So it does raise concern / flags.

Not to beat a dead horse, it sounds like you are aware, so I will drop it. If you want to discuss further, please feel free to reach out. I am happy to take a call or grab a coffee if you are ever in SF.

@yyx990803

This comment has been minimized.

Copy link
Member Author

commented Jun 26, 2019

@nijikokun options-based API may seem to provide "best practice out of the box", but over the years we've run into plenty of real world cases where it becomes the bottleneck for maintainability. I explained it in more details here. It makes sense for simple, single-responsibility components but actually becomes a maintenance bottleneck in large, multi-responsibility components.

So the options-based API has a smooth initial curve, but becomes a bottleneck in the long run - while the function-based API takes a bit more work upfront (in fact, we will be doing most of this work for you via our style guide) but pays dividends in the long run.

@nijikokun

This comment has been minimized.

Copy link

commented Jun 26, 2019

@nijikokun options-based API may seem to provide "best practice out of the box", but over the years we've run into plenty of real world cases where it becomes the bottleneck for maintainability. I explained it in more details here. It makes sense for simple, single-responsibility components but actually becomes a maintenance bottleneck in large, multi-responsibility components.

So the options-based API has a smooth initial curve, but becomes a bottleneck in the long run - while the function-based API takes a bit more work upfront (in fact, we will be doing most of this work for you via our style guide) but pays dividends in the long run.

@yyx990803 I have first hand experience with your statement, and actually agree with what you've messaged. That's why in many of my responses it has been a struggle to word that this isn't necessarily a negative but merely a contention point on continued use / adoption. I enjoy the new API, it actually fits within Typescript as I mentioned much earlier in the comments (two days ago?).

I chuckled because you responded right after I added an amendment to my message regarding this "in fact, we will be doing most of this work for you via our style guide". It's good to know that the same - at least I hope - level of best practice detail is put into this API, as well as a migration guide is being thought about. Words are hard here. Good to know you're thinking about it. Glad we chatted about it.

Looking forward to seeing the update. 👍

@mika76

This comment was marked as off-topic.

Copy link

commented Jun 26, 2019

@yyx990803 I have not made any personal attacks on you. This was a direct quote from your Twitter, and a relevant answer to the question.

I've called you a politician because you seem to think that you can write one thing on github and another on Twitter but we should all just ignore Twitter because it's "personal".

I've also said that you should reread the comments because you said you know what makes vue popular when practically 50% of the posts here are saying the opposite, and hence you should check your ego.

The way I see it all I have been doing is calling you out for bad behavior.

But I've had my say and will not comment any more.

@enekesabel

This comment has been minimized.

Copy link

commented Jun 26, 2019

@skyrpex I see these cons, especially the lack of improved type inference, but I still think the simplicity of the standard api outweighs these weaknesses, as long as a proper way to reuse logic is provided - which would be done by using composition.
When I chose Vue, it was mainly because I looked at its api and I understood it right away. This is totally a subjective, personal opinion, but if looked at Vue's docs, and the usage of the functional api was the first thing I see, I would have probably chosen something else - possibly Angular. I totally understand the benefits of the function api, and after working for a while with React hooks I totally embraced this kind of mindset. But I still think, while it was a step forward for React (made it less messy), it would a step back for Vue. It would take the main benefit of Vue - imo - ease of use and simplicity away.
That's why I'd prefer keeping the object api the primary solution, in case there's a way to introduce component reusability with a better pattern than the current ones.

@yyx990803

This comment was marked as off-topic.

Copy link
Member Author

commented Jun 26, 2019

@mika76

The tweet was obviously only targeting the subset of corporate developers that started complaining without properly going through the RFC, it does not represent my attitude towards all corporate adoption. You know that. Quoting it out of context as "my attitude towards corporate adoption" is passive-aggressive.

I am a human, I express emotions. GitHub is the professional scenario for me so I refrain from that as much as I can. In comparison, my personal Twitter does represent my personal opinions but they do not represent what the Vue project or the team thinks collectively. I think that's a perfectly normal thing to do (see many company employees' Twitter accounts w/ disclaimers like "opinions are my own"). I don't see the need for calling someone a politician the way you did because of that.

Regarding who knows why Vue got traction better than me - I'm still curious to know who that person would be. I was just stating an honest opinion - it would be weird for a project lead to not believe he knows what made the project successful. I still don't understand why you got all worked up and asked me to check my ego.

What I felt is continuous animosity towards me, expressed in the middle of normal technical discussions. This doesn't help anyone in anyway.

P.S. I'd be totally fine if you keep doing this on Twitter - but not on GitHub.

@ycmjason

This comment has been minimized.

Copy link

commented Jun 26, 2019

Start to worry about the integrity of "custom hooks". Sometimes, we want the values returned from a "custom hook" to be read-only.

From the example in the RFC, useMouse returns a value wrapper. I assume this also means that it is possible to manipulate the value of x and y outside of useMouse which is obviously violates the integrity of useMouse as we can no longer trust the values of x and y come from useMouse.

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

function useOtherLogic({ x, y }) {
  x.value = 30 // ooops, this mutates value from `useMouse`
  return { x, y }
}

// in consuming component
const Component = {
  setup() {
    const mousePos = useMouse()
    const { x, y } = useOtherLogic(mousePos)
    return { x, y }
  },
  template: `<div>{{ x }} {{ y }}</div>`
}

Is there any thought put into this yet? Such as readonly() which makes a value wrapper readonly?

@enekesabel

This comment has been minimized.

Copy link

commented Jun 26, 2019

@ycmjason I thick you can simply return a computed property based on that value in this case.

@ycmjason

This comment has been minimized.

Copy link

commented Jun 26, 2019

@enekesabel

Thought of that too. And we could easily make the readonly by doing

const readonly = (valueWrapper) => computed(() => valueWrapper.value)

I believe readonly() will make code more meaningful.

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: readonly(x), y: readonly(y) }
}

vs

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: computed(() => x.value), y: computed(() => y.value) }
}

As it will probably be a common case to use readonly() to maintain the integrity of the "custom hook", I recommend adding it to the official API.

@skyrpex

This comment has been minimized.

Copy link

commented Jun 26, 2019

I'm not sure it's a good idea. It's too easy to think that you could use it on objects or state...

@aztalbot

This comment has been minimized.

Copy link

commented Jun 26, 2019

I'm not sure it's a good idea. It's too easy to think that you could use it on objects or state...

Why wouldn't you use it on state? In my own experimenting with this API, I ended up writing a readonly helper to convert a state object into an object of readonly computed bindings – specifically to use as part of a Vuex-like store. i.e. everything exposed is a getter or mutation. I think readonly is a useful and explicit helper.

I could definitely foresee something like this (in which case readonly could also take a state object):

function useMouse() {
  const position = state({
    x: 0
    y: 0
  })
  const update = e => {
    position.x = e.pageX
    position.y = e.pageY
  }
  onMounted(() => {
    window.addEventListener('mousemove', update)
  })
  onUnmounted(() => {
    window.removeEventListener('mousemove', update)
  })
  return readonly(position) // convert state to object of readonly computeds
}

@ycmjason's example is also good, and something I myself would probably use.

This is easily implemented in user-land, which is fine, but it might have enough use cases to be included in the API.

@do-adams

This comment has been minimized.

Copy link

commented Jun 26, 2019

I think the proposal is looking great and will be very fun to write web components with!

Perhaps some of the negativity from the community centers around this new Function API being less "elegant" and "simple" than the current API, however it is a lot more powerful and flexible. I do wonder if it'll have a negative impact on the adoption of the library on Vue 3.x (which is what it is when things change), so that might be something to consider in the interest of the library itself in order to avoid a potential AngularJS to Angular 2.x situation.

However, the new API really does seem to make better use of JavaScript's functional strengths as a language. I don't see why we should cling to the strong object-oriented-patterns of old if there's a better way other than "familiarity" or the "but it's easier for beginners" when Vue still remains one of the more easier, intuitive, and enjoyable ways to write components for the web.

Thank you for your work, Evan!

@CyberAP

This comment has been minimized.

Copy link

commented Jun 26, 2019

@enekesabel

Thought of that too. And we could easily make the readonly by doing

const readonly = (valueWrapper) => computed(() => valueWrapper.value)

You can actually do this natively:

return Object.freeze({ x, y });

Vue can check if an object is frozen and prevent any external changes to the bindings.

@j0hnys

This comment has been minimized.

Copy link

commented Jun 26, 2019

@yyx990803 I can understand the decision and the rationale behind the move to a function-based API. It gives you a more maintainable and reusable codebase from the object-based API in the end for sure.

I can also understand the need for the object-based API to exist, as a vue developer for ~3 years and being responsible for teams of developers. You can navigate easily through it and people can be productive fast.

The great thing about the object-based API is that it gives you a clear place to where to put things and this is great for newcomers and experienced developers. If you think about it, the object-based API is more of a design pattern then it is an API. It is vue specific but it is a design pattern none the less, like MVC, DDD, TDD. That is what makes vue 2.0 attractive to many developers, it is way easier to find where something is because there are defined places to look for!

In the end software is made by people and it has to be easy to work with. If anything that I say can affect anything then what I would like to see is the new function-based API used when you really want to start optimizing your solutions or start large projects. The object-based API would remain as the official design pattern of vue so both can co-exist and supported.

Thanks you all for your time and especially Evan!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.