-
Notifications
You must be signed in to change notification settings - Fork 545
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
Conversation
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. |
Couldn't we detect if the returned value is a Promise or not and handle async component automatically without needing a factory function? |
In 3.0 that is essentially the case. It's implemented internally as a component.
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) |
Will it be also exposed as a component? |
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. |
There are no such plans, no. Maybe someone from the community would want to write such a babel plugin. |
Why drop |
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. |
@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> |
if we would do 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)); |
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)); |
Implementation wise, we will need to tell if a component is functional or classy, say: 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. |
@backbone87 if you are already using TS you can type the function using the import { FunctionalComponent } from 'vue'
const comp: FunctionalComponent<{ label: string }> = (props) => {
return h('div', props.label)
} |
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 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. |
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 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 |
@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) |
@backbone87 technically, the export default (props: { label: string }) => h('div', props.label) Or if you need slots, use the import { Slot } from 'vue'
export default (
props: { label: string },
slots: { default?: Slot }
) => h('div', [props.label, slots.default && slots.default()]) |
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:
(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: |
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 How would it be accessible with this proposal and #28? Edit: |
@hirokiosame yes - there will only be |
How Vue directives are handled? Is this the same as in Vue 2, when we have a |
@CyberAP I'm not sure how your question relates to this RFC, but yes |
I am probably mistaken but I don't understand why the support of I wrote a quick jsfiddle to express how i thought to maintain |
I am by no means a Vue ninja, but here's my understanding of this. In 2.x, <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 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>
<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 |
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. |
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:
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. |
@y-nk @privatenumber |
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. |
@privatenumber @clemyan i understand your point, and i do agree with it. thanks for all the clarifications! |
Will it still be possible to access the parent component from the render function of functional component? |
i want to strengthen my argument from #27 (comment) 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:
(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 |
This RFC is now in final comments stage. An RFC in final comments stage means that:
|
Would it be possible to use |
@CyberAP yes. |
@hearsay316 can you do a pull request? |
|
createAsyncComponent
API methodRendered