-
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
Component's public API #135
Conversation
Wait, Vue 2 & 3 already has a Anyway, is it good idea to incentive the coupling between parent and child? There are good reasons for loose coupling being considered an OOP good practice. About the RFC, I'd like to see a comparison with the use of |
Yes, current proposal is essentially extending
The primary goal of this proposal is to reduce component coupling and make it as explicit as possible. When you have explicit public interface it can be refactored with ease and all your dependencies are tracked.
With class API not officially supported I doubt it's feasible. |
Does it make sense that, to expose some data to my descendants (current behavior), I'd have to also expose it to my parent (proposed extension), and vice-versa? It seems leaky. Another con of both sharing the same name is that I'd imagine, too, that providing data has some (probably rather small) runtime penalty, even if it's not injected by any descendant. I'd like to be proved wrong on this. |
That's listed in the drawbacks section of the proposal. I'd be happy with an alternative
This interface should persist in the production mode. You should not be able to access component instance freely anymore.
That's true if we have any injections, since they're recursive they will cycle through all the provisions of every component in the parent tree starting from the injecting component to the root. |
Not sure if the proposed RFC is the best way to do that but I do like the idea of explicit exposing public component's methods so I hope it's at least good starting point for discussion. |
I like the goal - a lot actually. I was thinking about this very thing recently as a way to prevent foot guns like reaching into child element when unnecessary or testing internal state in unit tests. I’m not a fan of the semantics, though. I would want to expose everything to the render context and pluck certain fields from that context to expose on the instance publicly, but I’m not sure overloading provide and using symbols is the best way. I view both of those things as maybe too advanced for some users who might want a really simple way to interact with a child component in a legitimate use case. Maybe it doesn’t make sense, but I envisioned something like an I envisioned something like: const InputWrapper = {
template: `<input ref="input" >`,
expose: ['focus'], // parent can access the focus method
methods: {
focus() {
this.$refs.input.focus()
}
}
} or function useFocus() {
const focusable = ref(null)
function focus() {
focusable.value?.focus()
}
return {
focusable,
focus
}
}
const InputWrapper = {
template: `<input ref="input" >`,
setup() {
const { focusable: input, focus } = useFocus()
return {
input,
focus: markExposed(focus) // ensure parent can trigger focus
}
}
}
const InputWrapperParent = {
template: `
<InputWrapper ref="input" >
<button @click="focus"></button>
`,
components: { InputWrapper },
setup() {
const { focusable: input, focus } = useFocus()
return {
input,
focus // triggers focus on the input wrapper, which focuses the element, but parent of this element cannot call focus on this component
}
}
} These are just some ideas. I really like @CyberAP's idea; I'm just unsure about provide and symbols as the mechanism. Also, I am very curious about implementation and added type complexity (because I am guessing some mind boggling type definitions will be needed, although I hope not). |
Thanks, @aztalbot, I'm leaning towards the I was thinking that the
I was also thinking about mixins and $parent \ $root \ $children context access, should it be affected by this change or remain intact. I see the immediate benefits for mixin users, since we can control what data is exposed to mixins. With $parent I don't have a preference since I never use it. |
Is the ability to dig into a child component's full context really an issue? If someone does it and does something improper or ends up even changing behavior, that's their problem in the end, isn't it? They are misusing the component. Put another way, the API contract of a Vue component is implicit and doesn't need to be explicit. For instance, from the craziness I've seen, like some noob using private (as in with underscore prefixed) methods is very, very rare. And even when doing so, almost always things just don't work right or as expected. So, yeah, they may run into the footgun, but they back out of that attempt quickly and they'd usually learn to just compose a new component with the behavior they wanted. I feel this is trying to fix a problem that doesn't exist, and the motivational points of the RFC don't really offer real world examples of the concerns either. Granted, this is coming only from my personal experience and perspective and even though I've been working to support and help people with a Vue framework for several years, I don't consider myself an expert. As the saying goes, You can't design a system to avoid stupid. Stupid will always win. Ok. I just made that up. 😁 But, I think you get the point and I feel overcomplicating the component API for a very rare problem is adding an unnecessary cost to the development of components. Scott |
one more thought, just because I anticipate some folks may want complete tree shaking, we could extract an api for a component using a helper: import { createPublicComponent /* or some better function name */ } from "vue"
export default createPublicComponent({
// nothing special here, no new options
template: `<input ref="input" >`,
methods: {
focus() {
this.$refs.input.focus()
}
}
}, ctx => ({ focus: ctx.focus })) // a callback to extract fields you want to expose to the parent, if no callback, exposes everything @CyberAP, migration-wise, were you thinking a script could just find all usage $refs, and then add the necessary exports to that child component? @smolinari I don't disagree with you. For this RFC to be viable in my mind, it needs to be easy to migrate and very lightweight. Otherwise the gain isn't big enough. Maybe the reason I see the benefit is because I have seen folks I know hit these foot guns recently so it's fresh in my mind. From a testing perspective, I do think having an explicit public API is helpful so it's clear what internals need to be tested and what does not (i.e. you don't need to tell folks not to test implementation details because those details literally wouldn't be accessible unless it's exposed for use by other components). But yeah, these are small component design wins. |
Maybe coming up with some real world examples would be helpful? Explain where the implicit API wasn't enough and show how the suggested explicit API would be better. For sure that exercise will either strengthen your arguments and sell the suggestion better or maybe also change your mind on its overall value? Scott |
Components can have a lot of methods. Some of them can be considered as public and others as internals. The main gain IMHO would be IDE autocompletion where you could see just the safe (public) ones. so take for example Vuetify VTextField component, it would be nice if after typing |
@aztalbot, I'd better go with a warning in compatibility build, or some way of a I have rewritten the RFC to use |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not really the best expert to evaluate the content. But, I did find a small mistake. 😁
Scott
I’m wondering if this is better left to userland implementation via plugins and composition functions. I would think a plugin could add the expose option and also a refs option similar to https://github.com/posva/vue-reactive-refs/blob/master/README.md). The refs option could define validation and provide helpful errors in dev mode. When a component accesses a ref through this.$refs the only available fields would be the exposed ones (each ref would be proxied). For composition api, two helper functions This might not totally address whatever I had mentioned about testing, but that can be addressed in a similar way I’m sure. |
FeedbackGreat idea! I think tighter encapsulation of components is a step in the right direction. Currently, a parent component can access and manipulate everything about a child component. As a part of a framework's role to guide engineers towards better practices, I think it makes sense to prevent things like letting a parent component directly manipulate a child component's state and vice-versa.
Alternative considerations
|
@privatenumber thanks for your feedback! I have also considered the idea of sharing just methods to avoid directly manipulating other component's data. Probably the best way to deal with this would be printing an error in a console when you try to change component's state directly (stating that in order to change component's data prefer using exposed methods or passing down props), the same way it works with props mutations. Exposing just methods is too limiting in my opinion, since we can have computeds that could potentially contain important information about the component and to access those you'll need to call a method, which essentially duplicates existing functionality. But yes I believe everything exposed on a component should be read-only. Need more feedback on that issue before I add it to the RFC. Regarding |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for the RFC.
I don't think this is solving any existing problem. The 3 points mentioned in motivation are very similar and are all solved by documenting what is accessible inside of a component. By default, specially when using ui libraries, no internal components properties should be used. This doesn't concern some properties like $el
Maybe it's enough there's an easy built-in way to properly type the component so that only public data/methods are available in the public interface? |
Thanks for your input @posva.
This is unfortunately not true. You'll have to access component internals in order to focus an element. Doing querySelector or You'll also have to do this when using an uncontrolled component, like a self-closing dropdown. In order to forcefully close it you'll have to call And in case your component has these externally required methods there's no guarantee they will be present after the next refactor. If you do not specifically test for this then you'll even won't be able to tell that it's broken. |
It is. That's not the component internals (data, methods, etc). It's part of a Vue instance. I literally mentioned
Yeah, and that is fine as long as it is documented. That's why I said by default, as in, if no documentation is provided.
They will be there if they are part of the public api and there are documented. That's pretty much the whole point |
You can type The good parts are that interfaces are reusable and can be re-implemented by various components, and are totally erased during build (and, therefore, has zero runtime cost). The bad part is that I don't know how to disallow See demo: https://github.com/leopiccionia/typing-vue-with-interfaces Edit: I've tried to setup a CodeSandbox with Vue CLI and TypeScript support, but wasn't able to make it work. |
Thanks, but I'd rather see a demo without using vue-class-component |
And even worse you can't disallow any access to the instance. There's nothing stopping you from doing |
At this point, it's important to settle which is the primary goal of this RFC:
Those motivations are quite different and affect distinct users sets, so the earlier we settle this, the more productive will be the RFC discussion from then on. If we're only interested in avoiding unintended breaking changes and easing code refactoring, type checking + well-written documentation is a good enough combo. Linting rules can also go a long way to educate users about relying on other components' internals. If strict defensive programming is critical, we should ask if something like |
I wouldn't call that defensive programming but rather a contract programming. |
I created a POC for a way of defining a public API for use in Composition API, if anyone is interested in userland possibilities. |
Closing in favor of #343 |
Introduce a new
expose
option to declare component's public API.Rendered