-
-
Notifications
You must be signed in to change notification settings - Fork 8.4k
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
Class API #4
Comments
Doesn't using |
I'm all for it. Even for v2.0 and below (where methods are contained within their own object obviously), I always find myself itching for the convention. Another approach might be to use the denoting EDIT: But then again, users can add |
@znck |
While it's convenient (and I like the syntax), is it always the expected behavior? |
@phanan as long as we document this. |
IMO, we should not change the lifecycle method names in v3, because of we should be avoided migration fatigue to v3 from v2 as possible, however, as official blog mentioned, we will plan to support about the compatible features for 2.x, so I think It maybe not much of the problem. Additionally If we will be able to support the migration tool, it's wonderful. |
While I hear this, we might as well keep in mind that these kinds of backward API changes can only be introduced with major versions, which means if we don't take this opportunity, we'll have to wait for v4 (another 2 years perhaps?) 😄 |
To me, it looks very hacky to keep some options top-level and some under an static props: ComponentProps<App> = {
// ...
}
This pains me. 😅 Since we're relying on a stabilized static props = {
// ...
}
This probably isn't possible or would come with too many edge cases, but in TS, I think it'd be great if we could avoid defining types twice for each prop. Is there possibly some way (e.g. with a fancy TS compiler option) that we could access TS type info at runtime in development?
I agree with @kazupon in this instance. We already have a nice convention for lifecycle methods that differentiate them from methods: either past tense verb, or
@phanan I agree with this, but I still think there has to be significant benefit if we're going to force users to relearn something. |
The naming difference could be adapted through
Agree. |
@chrisvfritz static class fields are not in the spec yet. Currently in plain ES you can only do static getters.
Maybe. Having the options as an object is easier for compatibility reasons. Making them flat involves extracting these flat properties back into an object for easier reflection.
This is unlikely because the static types and the options serve slightly different purposes:
|
Edit: changed options to be flat static properties on the class |
From the perspective of vue-class-component, builtin lifecycle names haven't caused many problems or confusion. On the other hand Computed properties can also have a lazy option. But it looks like Vue 3 will make computed lazy by default. https://github.com/vuejs/vue-next/blob/bf38fea31388dd8c3a2f4206221089a2ed92e841/packages/observer/src/computed.ts#L9 |
@yyx990803 I might have misunderstood, but I thought we were waiting to release Vue 3 until class features had stabilized. Field declarations and static class fields are both stage 3 right now, so I was assuming we'd be able to rely on them by the time of release. To me, field declarations (both static and instance-based) are an absolutely essential feature of any class API from a DX perspective - and for the record, these are the only existing class proposals I feel that way about. Beyond breaking the logical options order we've recommended for readable components, tacking on extra properties after the initial definition is hacky enough that I think it'd become impossible to sell classes as an improvement. We're already going to get a lot of pushback from people who prefer the object-based API. It's so simple and well-organized that outside of TS, it's rare to hear anything but praise for it. That means probably most users will feel threatened initially, because they'll see classes as a step towards deprecating the object-based API they love (and their assumption is probably accurate). For that reason, I feel like the experience has to be at least as good with classes. |
Ideally, we want to wait until class fields become part of the spec before releasing 3.0. I'm thinking somewhere around mid 2019, and with them being currently stage 3, it's unlikely for them to make it into ES2019. However, it's hopeful that they are stage 4 around that point, and that would be safe enough to recommend usage via transpilers (which most serious users will still need, for the time being). If we've reached a day where most users are shipping non-transpiled code, I'm pretty sure class fields would have landed by then. Also, we can actually make static getters work: class Foo extends Component {
static get props() {
return {
msg: String
}
}
} Finally, we are not going to deprecate object-based syntax in 3.x, those who prefer it can keep using it. As I said, this is the less "feeling threatened" way to look at it: Components have always been internally represented as classes, even in 2.x (as function constructors). We are just converting options objects into classes. However, in 2.x there's no way to directly author this internal class using the class syntax. 3.0 provides a way to do that, while still keeping the option to use objects. Note that while there are users who strongly prefer the object syntax, there are other users who strongly prefer the class-based syntax too (especially for users coming from languages where class is the norm). Shying away from classes essentially sacrifices the DX for the class-preferring group where the only benefit is making the object-preferring group feel "comfortable" or "safe". |
@yyx990803 As I understand it, one of the main reasons we're defaulting to a class-based API is to prevent the schism where TypeScript users have to use a significantly different interface from everyone else. I agree that's a problem worth solving. However, if we're unable to sell the benefits of the class-based API to a large group of users, we'll actually have failed to erase that schism. Instead, I think we'll actually have made it worse, because then the cognitive burden will have shifted from some of our most advanced users (TypeScript users) to our least advanced users (those who either feel intimidated by classes or have bad experiences with class components early on). Being the least advanced, they'll have a much, much harder time bridging the gap and translating examples - especially since unlike current TypeScript users, they'll actually have to learn new, unfamiliar language features to do so. This is why I feel that all major class features that affect component definitions should be stable and usable in browsers before we release Vue 3. Otherwise, even if we keep unstable class features out of the docs, there will still be unstable features in community library examples, blog posts, Twitter, etc - written by people using Babel. As proven in the React community, this is inevitable when an unstable feature is more convenient or enables a useful pattern. And in this case, using a field declaration with: static props = {
msg: String
} is just more convenient than: static get props() {
return {
msg: String
}
} Even I'd probably be defining my props using the more convenient field declarations, because they're simpler and also more semantically correct, since a getter is only necessary as a hack to get around the more appropriate feature being unavailable sometimes. And so then I fear we'll have:
However, if everyone can just use the correct and convenient class features, migrating to class-based components will be much less painful for non-transpilers and we simulataneously avoid confusion for Babel users.
But as I said, this would be a step towards deprecating the object-based API, right? If we moved to a default class-based API in Vue 3, because we want to get everyone using the same syntax in both JS and TS, I'd be very surprised if we kept the object-based syntax for Vue 4 - so their fears would be warranted.
I don't feel like that's entirely accurate. 😕 If we're moving to classes as the new recommended default for all users, we're doing a lot more than "providing a way" to directly author internal classes - we're creating a significant cost to not using classes, since that's what most public examples will use. I think there are ways to sell classes as an improvement, but it'll be much, much harder if non-transpiling users are stuck using only a subset of the essential DX features for classes and due to instability, often hitting roadblocks as they struggle to learn them.
TypeScript users aside, it sounds like these JS users would feel more "comfortable" or "safe" using classes. 😉 (But as a sidenote, I came from languages where classes are central, which is part of the reason I've been so frustrated with the instability and generally unfinished state ES classes have been in, resulting in poor DX.)
I'm not shying away from classes at this point. Personally, I still see many more disadvantages than advantages for JS users, both as an educator and developer, but I'm trying really hard to have faith and figure out how to make the transition as smooth as possible. |
How about removing now: class App extends Component {
// data
data() {
return { count: 0 }
}
} new: class App extends Component {
constructor() {
// data
this.count = 0;
}
} Does it do the same thing with ES6 Proxy now? Thanks. |
I really love @Jinjiang 's proposal and actually I have also considered it. Pros are evident: it removes Current implementation only assumes component being One straight forward way to collect reactive data is to However, users cannot use props in constructor to initialize data, if component class does not extend Vue's |
While I personally like the idea, I'm not sure about this practice. Constructors can, and often, do more than just initializing the data. Also, not every piece of data initialized in the constructor needs to be reactive, so implicitly treating them that way instead of using an explicit EDIT: While we're at it, constructors are more like |
Constructors in general can do a lot of things. But component class isn't general purpose, it is for binding view with data. For component constructors, most of them should do few things (or be dumb). React advises users only initialize state or bind methods. https://reactjs.org/docs/react-component.html#constructor Angular also instruct users to use constructor as state initialization and dependency injection. None of them are implementing business logic. Component class also has lifecycle method like
We can treat the new That said, I'm still ambivalent toward the proposal of treating constructor as data. We will have to rely on hacky or dirty implementation to keep backward-compatible and feature complete(like using prop without inheriting). |
And as a step forward, we can simplify the lifecycle design by deprecating EDIT:
@phanan sorry for missing your comment. It seems we think the same. 😅 |
@chrisvfritz ok, I may have misunderstood your intention. From what you have explained, it seems your concern has less to do with the actual implementation, but more about how we present / document / transition the users to the new syntax. So let's clarify a few points:
Does that make sense? |
@Jinjiang @HerringtonDarkholme @phanan I've also considered this. Some reasons I am hesitant about it:
The biggest advantage is with field initializers + decorators the usage is clean, expressive AND type-checking friendly. The biggest problem is usage is only nice with field initializers + decorators. Plain ES usage is much more verbose. |
Ok I've managed to make both // plain ES2015, use `data()` for more concise code
class Foo extends Component {
data() {
return {
count: 1
}
}
}
// TS or with Babel, use initializers for the nice syntax
class Foo extends Component {
count: number = 1
} With benchmarking the performance cost is in acceptable range (~10ms for 3000 component instances) One caveat: if you want to access props in initializers, you can, but you must do so via interface Props {
value: number
}
class Foo extends Component<Props> {
count: number = this.$props.value + 1
} |
Class education/transition
@yyx990803 That makes perfect sense, clarifies quite a few things for me, and generally resolves my education/transition concerns. 🙂 And if we don't end up making classes the recommended default for JS users, all my concerns actually disappear. I'll keep thinking about what might be an ideal learning path and then create a separate issue to discuss that. Alternatives for
|
That's a very good point. IMHO reactivity should be opted-in instead of opted-out. |
You don't really need them (especially in plain ES), but:
|
Sorry, just to clarify for myself, is the idea that plain property declarations with initializers would automatically be marked as reactive? I think that would be way better from a UX POV, as well as a type system POV. I guess the downside is that this really could only work in a Proxy-based reactivity model; you couldn't swap another reactivity model in if you wanted to support older targets, but I don't know how much weight that holds. |
@DanielRosenwasser yeah, plain initializers would become reactive. I think it's possible to work in older model too, as we are already doing that in |
Update: don't know since when but latest Chrome Canary now enables class fields by default, so we can already play with the following API without a transpiler: class Foo extends Component {
static props = {
msg: String
}
count = 0
render() {
return h('div', [this.msg, this.count])
}
} |
So the import { h, Component, ComponentWatchOptions } from '@vue/renderer-dom'
interface Props {
msg: string
}
class App extends Component<Props> {
static props = {
msg: String
}
// data fields
count: number = 0
// ComponentWatchOptions type is only needed if `this` inference
// is needed inside options, e.g. in watch callbacks
static watch: ComponentWatchOptions<App> {
count(value) {
console.log(value)
}
}
created() {
console.log(this.count)
}
get plusOne() {
return this.count + 1
}
increment() {
this.count++
}
render(props) {
// ...
}
} |
@Jinjiang it can still be useful when you somehow have to use |
Oh, so |
@chrisvfritz I think, Evan meant |
Don't think this concern by @chrisvfritz is addressed. I doubt TS users would use the Are both cases below valid usages in 3.0? interface Props {
msg: string
}
// A
class App extends Component {
render(props: Props) {
// ...
}
}
// B: For uses without render function
class App extends Component<Props> {
} Also, would the class API support |
@octref yes, that one still needs some more work. First, runtime props option works the same way as in v2: class App extends Component {
static props = {
msg: {
type: String,
default: 'foo',
validator: str => ['foo', 'bar'].includes(str)
}
}
} Second, in the current prototype the behavior changes based on whether
So technically, props type inference on |
I think this API would be cleaner. It's the same in JS/TS usage. class App extends Component {
// props
static msg = 'foo';
// data
count = 0;
} TS advanced usage, for more expressiveness: type T = 'foo' | 'bar';
class App extends Component {
// no need to write `static msg: string = 'foo'`, as TS infers type of `msg`
static msg = 'foo';
// use TS for more complex types
static msg2: T = 'foo';
// runtime prop options for JS/TS
static msg3 = {
deafult: 'foo',
validator: (s: string) => ['foo', 'bar'].includes(s)
};
} I think this better aligns Vue's API with the class semantic. I also like that class App extends Component {
static msg;
static msg2;
} |
@octref static properties are mapped to options, e.g. class App extends Component {
static template = `<div>{{ msg }}</div>`
} I don't think static properties are semantically correct for props. |
@yyx990803 I reread some discussion in #2 and #3. This Comment was helpful.
So are both cases below invalid, because class App extends Component {
static template = `<div>{{ msg }}</div>`
} <template>
<div>{{ msg }}</div>
</template>
<script>
export class App extends Component {}
</script> Whereas in TS, does the below case mean:
interface Props { msg: string }
class App extends Component<Props> {
static template = `<div>{{ msg }}</div>`
}
I do think static properties map semantically to props option but not props as bound to |
Correct. It may be on
No. This is a part where the type is currently mismatched with the runtime behavior. Declaring a props interface currently makes the props available on both The ideal situation is:
|
Purely with types I don't know. I can give it a try together with @DanielRosenwasser.
I'm not sure, for me I think https://github.com/itsFrank/vue-typescript rather than vue-class-component is what most TS users want. For example: interface Props { msg: MsgItem }
class App extends Component<Props> {
static props = ['msg'];
} What TS users want is likely in the template, the |
The problem with decorators is that the proposal still faces a lot of uncertainty at this point, so I'd like to avoid relying on it. I think it makes sense for the interface to augment interface Props { msg: string }
class App extends Component<Props> {
created() {
// make use of this.msg, assuming it's a string
}
} where it's valid in types but actually it'd be |
Closing in favor of #20 |
This is a reference of the current implemented version and open to discussion.
Update: don't know since when but latest Chrome Canary now enables class fields by default, so we can already play with the following API without a transpiler.
Plain ES usage:
TS Usage:
The text was updated successfully, but these errors were encountered: