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

Functional and async components API change #27

Merged
merged 3 commits into from
Nov 12, 2019

Conversation

yyx990803
Copy link
Member

@yyx990803 yyx990803 commented Apr 8, 2019

  • Functional components must be written as plain functions
  • Async component must be created via the createAsyncComponent API method

Rendered

@yyx990803 yyx990803 added 3.x This RFC only targets 3.0 and above breaking change This RFC contains breaking changes or deprecations of old API. core labels Apr 8, 2019
@posva
Copy link
Member

posva commented Apr 8, 2019

I'm always sceptical regarding the async factory. I always felt it's something that could be handled better in userland or through a component with a declarative approach. But at the same time, since the API is now tree shakeable, it seems fine.

@Akryum
Copy link
Member

Akryum commented Apr 8, 2019

Couldn't we detect if the returned value is a Promise or not and handle async component automatically without needing a factory function?

@yyx990803
Copy link
Member Author

yyx990803 commented Apr 8, 2019

I'm always sceptical regarding the async factory. I always felt it's something that could be handled better in userland or through a component with a declarative approach. But at the same time, since the API is now tree shakeable, it seems fine.

In 3.0 that is essentially the case. It's implemented internally as a component.

Couldn't we detect if the returned value is a Promise or not and handle async component automatically without needing a factory function?

I think that can be brittle, and it makes the code harder to read for a human (you have to read the code to see if it's returning a Promise to tell what kind of component it is)

@Akryum
Copy link
Member

Akryum commented Apr 8, 2019

Will it be also exposed as a component?

@olavoasantos
Copy link

Are there plans to have some kind of loader that allows us to write vue templates as the returned statement of the function? (like we do with JSX)

props => (
  <div class="msg" v-if="props.isVisible">hidden message</div>
) 

I feel this could stimulate the usage of functional components.

@LinusBorg
Copy link
Member

There are no such plans, no. Maybe someone from the community would want to write such a babel plugin.

@houd1ni
Copy link

houd1ni commented Apr 8, 2019

Why drop <template functional />? Seems, that the RFC just drops it in favor of JSX, and there's no bright difference between template without <script /> and JSX/function in terms of functional expectations (issue 2 of motivation). Also, many of vue users don't like jsx/render functions especially for their visual verbosity, hence this way of declaration doesn't seem to be simpler.

@LinusBorg
Copy link
Member

As hinted at in the RFC: normal components supports fragments and have practically similar performance in 3.0

So the use case for writing functional components is mostly reduced to writing a utility with a raw render function, likely involving more dynamic stuff you can't do in a template.

For everything else just use a normal component.

@yyx990803
Copy link
Member Author

@olavoasantos with #25 (optional props declaration) you will be able to write SFC templates like this:

<template>
  <div class="msg" v-if="$props.isVisible">hidden message</div>
</template>

@yyx990803 yyx990803 changed the title Functional and async components api change Functional and async components API change Apr 9, 2019
@backbone87
Copy link

backbone87 commented Apr 9, 2019

if we would do export default function(props) => h('div', props.label); in TS with strict settings, there would be complains about implicit any and zero implied typing. While enforcing manual typing for props would be fine, it is much more annoying to type slots (because they are always VNode returning functions).

I propose a similar approach like with async components:

import { createFunctionalComponent, h } from 'vue';

export default createFunctionalComponent((props) => h('div', props.label));

This gives at least some default typing and also provides an interception point for the framework in the future.

edit: this would also allow an easier declaration of props:

import { createFunctionalComponent, h } from 'vue';

export default createFunctionalComponent({ label: String }, (props) => h('div', props.label));

@backbone87
Copy link

building up on the stuff from other RFCs a fluent API for creating functional components could also be provided:

import { prop, inject, h } from 'vue';

const a = prop('label', String).render(({ label }) => h('div', label));
const b = prop('label', String)
  .prop('strong', Boolean, false)
  .render(({ label, strong }) => h('div', { class: strong ? ['strong'] : [] }, label));
const c = inject(ThemeSymbol, 'theme').render(({ theme }) => h('div', { class: theme.class }));
const d = prop('label', String)
  .inject(ThemeSymbol, 'theme')
  .render(({ label, theme }) => h('div', { class: theme.class }, label));

@HerringtonDarkholme
Copy link
Member

HerringtonDarkholme commented Apr 9, 2019

Implementation wise, we will need to tell if a component is functional or classy, say:
const initiateComponent = component => component instanceof Vue ? new component() : component(props). Is this right?

Another tantalizing misusage might be something like "async functional component". Like:

const userProfile = async (props) => {
  const profile = await fetchUser(props.userId)
  return h('div', user.name)
}

I don't know if it will be misused in real projects. But it certainly warrants async factory. Since async component's promise is resolved only once and cached for later use, while async function component will need be resolved multiple times.


@backbone87 Thanks for the idea! But I think fluent API can be provided by community. The API will be very thin.
Actually I've done something similar myself for Vue 2.0 's type safety. https://github.com/HerringtonDarkholme/vivio

@yyx990803
Copy link
Member Author

@backbone87 if you are already using TS you can type the function using the FunctionalComponent type provided by Vue (instead of forcing an extra API for non-TS users):

import { FunctionalComponent } from 'vue'

const comp: FunctionalComponent<{ label: string }> = (props) => {
  return h('div', props.label)
}

@backbone87
Copy link

backbone87 commented Apr 9, 2019

then you still need an extra line for the export. i just wanted to point out that this planned API is "unfriendly" to TS users. additionally if you do const comp: FC<...> then generic type inference is not possible (at least when i checked it last time).

also the benefit of this "extra API" is an interception point for the framework to harden BC for future changes. but like @HerringtonDarkholme said, this can also be done userland, so i am fine without it being in the framework.

@olavoasantos
Copy link

olavoasantos commented Apr 9, 2019

Sorry if this is out of the scope of this thread.

@yyx990803 I think the only caveat of that approach is the fact that the <template> gets isolated, making it difficult to access an imported variable. Ideally (for this kind of function-like components) I would love to be able to do something like:

import { h } from 'vue'
import store from './path/to/store'

const CustomButton = ({ label, isDisabled }) => (
  <button :disabled="isDisabled" @click="store.dispatch('yourAction')">
    {{ label }}
  </button>
)

CustomButton.props = {
  label: String,
  isDisabled: Boolean,
}

export default CustomButton

Currently, we can achieve this through render functions or JSX. But I do miss the readability and love of v-ifs and @clicks. But, as @LinusBorg suggested, this could probably be achieved through a custom loader made by the community.

@yyx990803
Copy link
Member Author

@olavoasantos you can probably fork the JSX plugins to support alternative syntax (e.g. https://github.com/vuejs/jsx/tree/dev/packages/babel-sugar-v-on)

@yyx990803
Copy link
Member Author

yyx990803 commented Apr 10, 2019

@backbone87 technically, the FunctionalComponent type is only necessary if you want to type all 4 arguments. If your functional component uses only props, it's totally fine to type it like this (this even works with TSX):

export default (props: { label: string }) => h('div', props.label)

Or if you need slots, use the Slot type:

import { Slot } from 'vue'

export default (
  props: { label: string },
  slots: { default?: Slot }
) => h('div', [props.label, slots.default && slots.default()])

@backbone87
Copy link

backbone87 commented Apr 10, 2019

ultimately it is still a signature that is enforced by the framework and applying the basic types in userland is error prone, because one needs to know the expected signature:

// WOOPS this is the wrong signature and TS cant help us here
export default (props: Props, slots: Slots, vnode: VNode) => h();

(edit: but like already mentioned above, my wrapper function proposal can be done in userland)

i also just noticed another DX annoyance in your 2nd example: slots.default && slots.default({ someProp: 42 }) is pretty boilerplate and slots('default', { someProp: 42 }) could be a nice shortcut for this

@privatenumber
Copy link

privatenumber commented Apr 12, 2019

One of my favorite benefits of the functional component is having nuanced control over where the "class" attribute gets inherited to. In v2, class data is in the class or staticClass property under ctx.data.

How would it be accessible with this proposal and #28?

Edit:
Looks like this is largely answered here. Would class and staticClass be consolidated?

@yyx990803
Copy link
Member Author

@hirokiosame yes - there will only be class and it will be in this.$attrs or the 3rd argument to a functional component.

@CyberAP
Copy link
Contributor

CyberAP commented Apr 18, 2019

How Vue directives are handled? Is this the same as in Vue 2, when we have a v-text it becomes textContent?

@yyx990803
Copy link
Member Author

@CyberAP I'm not sure how your question relates to this RFC, but yes v-text compiles to a textContent prop.

@y-nk
Copy link

y-nk commented May 10, 2019

I am probably mistaken but I don't understand why the support of <template functional> should be dropped since it looks to me as a keyword to handle properly template compilation (?) Isn't it more related to vue-loader and if so, is it really necessary to drop it ?

I wrote a quick jsfiddle to express how i thought to maintain <template functional> keyword while implementing this new api, but that could be wrong in so many ways...

@clemyan
Copy link

clemyan commented May 10, 2019

I am by no means a Vue ninja, but here's my understanding of this.


In 2.x, <template functional> signifies a non-trivial change in template compilation. For example this SFC

<template functional>
  <div>{{ props.text }}</div>
</template>
<script>
  export default {
    props: { text: { required: true, type: String } }
  }
</script>

compiles roughly to this

export default {
  functional: true
  props: { text: { required: true, type: String } }
  render(h, context) { return h('div', [ context.props.text ]) }
}

While the stateful equivalent is this

<template>
  <div>{{ $props.text }}</div>
</template>
<script>
  export default {
    props: { text: { required: true, type: String } }
  }
</script>
export default {
  props: { text: { required: true, type: String } }
  render(h) { return h('div', [ this.$props.text ]) }
}

The functional flag on <template> determines the context (this vs passed context) and we have to drop the $ on $props because the context is different. (Yes, I know we can just use {{ text }}. The inclusion of $props is deliberate and highlights the difference in the render context)


In 3.x, the call signature and context will be the same for stateful and functional components. Using the same example, this stateful comp

<template>
  <div>{{ text }}</div>
</template>
<script>
  export default {
    props: { text: { required: true, type: String } }
  }
</script>

will compile (roughly) to

import { createElement as h } from 'vue'
export default {
  props: { text: { required: true, type: String } }
  render: (props) => h('div', [ props.text ])
}

On the other hand, if <template functional> were supported, this functional template

<template functional>
  <div>{{ text }}</div>
</template>
<script>
  export default {
    props: { text: { required: true, type: String } }
  }
</script>

would compile (roughly) to this

import { createElement as h } from 'vue'
const F = (props) => h('div', [ props.text ])
F.props = { text: { required: true, type: String } }
export default F

Notice how the render function is exactly the same. We are just exporting stuff in a different "format", so to speak. And, as mentioned in this RFC, the performance-wise difference between these is negligible, so there is no longer an incentive to use the latter over the former. As such, the maintenance cost of supporting <template functional> would outweigh the benefits.

@y-nk
Copy link

y-nk commented May 10, 2019

In 3.x, the call signature and context will be the same for stateful and functional components. Using the same example, this stateful comp

I'm missing a point there. If the signature is the same, then why is there a difference ? All components should be declared as stateless because... they are (and then design internal state mechanism as a mixin) ? I'm surely missing a key point. If this is out of topic, let me know.

@privatenumber
Copy link

@y-nk

I am probably mistaken but I don't understand why the support of <template functional> should be dropped since it looks to me as a keyword to handle properly template compilation (?) Isn't it more related to vue-loader and if so, is it really necessary to drop it ?

I wrote a quick jsfiddle to express how i thought to maintain <template functional> keyword while implementing this new api, but that could be wrong in so many ways...

I believe the reason to drop it isn't a technical one but more of a product decision of how Evan intends Vue and functional components to be used.

My understanding is:

  • With the increasing support for functional components, the difference from SFC (stateful component) was diminishing
  • In v3
    • SFC will support fragments and control over attribute inheritance (SFCs will be able to do everything a functional component can do in v2, and more)
    • Functional components will do less. No SFC (just a .js file), no template, no styles, etc.
  • Stateful components are apparently faster to initialize

At this point, there's almost no reason to use a functional component. I'm interested to see what use-cases for it will arise in v3.

@clemyan
Copy link

clemyan commented May 11, 2019

@y-nk @privatenumber
After thinking about it for some time, I am thinking there are no practical use cases for functional components anymore in v3, if the performance difference is actually negligible. I guess slots are technically functional components but other than those, I don't see where functional components would be preferred over stateful components.

@LinusBorg
Copy link
Member

The dominant use case would probably be to create wrapper components that more or less transparantly wrap another component.

That could be done very elegantly with a function.

@y-nk
Copy link

y-nk commented May 16, 2019

@privatenumber @clemyan i understand your point, and i do agree with it. thanks for all the clarifications!

@SeregPie
Copy link

Will it still be possible to access the parent component from the render function of functional component?
In 2.0 it is context.parent.

@backbone87
Copy link

backbone87 commented Jul 15, 2019

i want to strengthen my argument from #27 (comment)
In https://github.com/vuejs/rfcs/blob/function-apis/active-rfcs/0000-function-api.md#type-inference-1 the same technique is used to allow type inference.

type VNode = any;
type AnyAttrs = Record<string, any>;
type NormalizedComponent = any;
type PropsDefinition = any;
type PropsFrom<Definition extends PropsDefinition> = any; // inference magic
type SlotFunction = (props: Record<string, any>) => VNode[];
type Slots<Names extends string> = { [x in Names]: SlotFunction };
type RenderFunction<Props, SlotNames extends string, Attrs> = (
  props: Props,
  slots: Slots<SlotNames>,
  attrs: Attrs,
  vnode: VNode,
) => VNode | VNode[];
type RenderFunctionWithoutProps<Attrs, SlotNames extends string> = (
  attrs: Attrs,
  slots: Slots<SlotNames>,
  vnode: VNode,
) => VNode | VNode[];

declare function h(): VNode;
declare const createFunctionalComponent: {
  <Attrs extends Record<string, any> = {}, SlotNames extends string = string>(
    render: RenderFunctionWithoutProps<Attrs & AnyAttrs, SlotNames>,
  ): NormalizedComponent;
  withProps<Definition extends PropsDefinition>(
    propDefinition: Definition,
  ): <Attrs extends Record<string, any> = {}, SlotNames extends string = string>(
    render: RenderFunction<PropsFrom<Definition>, SlotNames, Attrs & AnyAttrs>,
  ) => NormalizedComponent;
};

interface FontSize {
  fontSize?: string | number;
}

const A = createFunctionalComponent((attrs, slots, vnode) => {
  attrs.someAttr; // any
  return h(/* ... */);
});
const B = createFunctionalComponent<FontSize, 'default' | 'headline'>((attrs, slots, vnode) => {
  attrs.fontSize; // string | number | undefined
  attrs.other; // any

  slots.default; // SlotFunction
  slots.headline; // SlotFunction
  slots.other; // error

  return h(/* ... */);
});

const C = createFunctionalComponent.withProps({})((props, slots, attrs, vnode) => h(/* ... */));
const D = createFunctionalComponent.withProps({})<FontSize, 'default' | 'headline'>((props, slots, attrs, vnode) =>
  h(/* ... */),
);

// alternative
const E = withProps({}).createFunctionalComponent((props, slots, attrs, vnode) => h(/* ... */));
const F = withProps({}).createFunctionalComponent<FontSize, 'default' | 'headline'>((props, slots, attrs, vnode) =>
  h(/* ... */),
);

in addition to strong typing and a "component creation DSL", if we require the usage of these factory functions, there are more benefits:

  • entry point for compatibility layers (forward & backward)
  • normalization on definition -> much easier implementations for component consumers.
  • optimization: normalization could maybe be done at build time (like webpack can do with preexecuted static functions)

(ofc the example is limited to functional components, but it can be extended for all kinds of components)

edit: regarding the different render function signature: i just dont agree with #25, so i showed how this can be solved on another level. if #25 gets accepted anyway, then it would result in just a single signature on this side

@yyx990803 yyx990803 added the final comments This RFC is in final comments period label Nov 6, 2019
@yyx990803
Copy link
Member Author

This RFC is now in final comments stage. An RFC in final comments stage means that:

  • The core team has reviewed the feedback and reached consensus about the general direction of the RFC and believe that this RFC is a worthwhile addition to the framework.
  • Final comments stage does not mean the RFC's design details are final - we may still tweak the details as we implement it and discover new technical insights or constraints. It may even be further adjusted based on user feedback after it lands in an alpha/beta release.
  • If no major objections with solid supporting arguments have been presented after a week, the RFC will be merged and become an active RFC.

@CyberAP
Copy link
Contributor

CyberAP commented Nov 7, 2019

Would it be possible to use <Suspense /> in conjunction with async components? (Similarly to the sync components that just return an async setup)

@yyx990803
Copy link
Member Author

@CyberAP yes.

@hearsay316
Copy link

[3.0.0-alpha.11] *** runtime-core: rename createAsyncComponent to defineAsyncComponent** (#888) (ebc5873)

recording

@posva
Copy link
Member

posva commented Apr 23, 2020

@hearsay316 can you do a pull request?

@hearsay316
Copy link

@hearsay316 can you do a pull request?
I studied vue3 and found that he had changed his name in the update, and felt that he should be recorded.

@yyx990803 yyx990803 removed the final comments This RFC is in final comments period label Jul 1, 2020
@bencodezen bencodezen mentioned this pull request Jul 6, 2020
25 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
3.x This RFC only targets 3.0 and above breaking change This RFC contains breaking changes or deprecations of old API. core
Projects
None yet
Development

Successfully merging this pull request may close these issues.