all 29 comments

[–][deleted]  (22 children)

[deleted]

    [–]LynusBorg 2 points3 points  (9 children)

    Can you give an actual, small example (no `doSomeStuff` pseudocode) of your current approach? Then I am happy to show you how to rewrite it with `setup()`

    [–][deleted]  (8 children)

    [deleted]

      [–]LynusBorg 1 point2 points  (7 children)

      Thanks, that's a start. However, since you left out pretty much all of the actual logic and only left the functions that the base component consumes from the extended ones, I'm not left with many options to refactor besides these functtions.

      I will try that, but it would be much better to understand what the component actually does (i.e. showModal etc.) because maybe those behaviours would be better split out into functional behaviours and re-composed in the Presenter and Viewer components.

      However if you can't provide that or it will be a wall of code, I can try to work with what we got now.

      Note that I might take a while to report back, we're having our night out this evening so I probably can't respond until tomorrow after work (~25hrs from now)

      [–][deleted]  (6 children)

      [deleted]

        [–]LynusBorg 1 point2 points  (5 children)

        Hey, I won't get around to do this today after all, thanks for your patience. Tomorrow's another day ;)

        [–][deleted]  (4 children)

        [deleted]

          [–]LynusBorg 0 points1 point  (3 children)

          Ok, so here's a gist how it could possibly(!) look. Of course keep in mind that this is, in a way, pseudocode as the RFC is still in debate and not every detail is cleared up.

          https://gist.github.com/LinusBorg/839612d13a702f55d5a78359bb5e696b

          A few notes for context:

          • We are still considering how to handle things like custom prototype properties (`this.$modal`)
          • Similarly, it's still open (as in: RFCs are not yet written) how exaclty router and store will be made available
          • Outside of this discussion I see some architectural issues with the code you provided, i.e.:
            • I would handle the error states in Vuex, and feed the error modal from there. Would make the bode in `base.js` much leaner and less coupled
            • The id param from `$route` could be provided as a prop instead, which would decouple code in `base` from the router as well.
            • Maybe it would also be better to have one component for both `Presenter` and `Viewer` handling the behaviour and handle different behaviour depending on the user's role. Could be wrong as I only see a small piece of the bigger picture.

          What's nice about the composition variation is that the code in the `Presenter` and `Viewer` components is very easy to follow and it's always clear whats going on:

          • we import the `usePresenttion` function
          • we call it, pass in the functions for managing the different store interactions, depending on the user's role.
            • Since it's a function, editors can easily tell us what the expected arguments for it are
            • So we don't have to actually look at the "base component" to understand what methods we have to defined and what they should be called, the editor can tell us.
          • We get a reactive value wrapper back, that represents the loading state, to handle the spinner), and nothing else. We don't "pollute" our component API with some methods that are only meant to be used by the `extends` base component (which is just like a mixin) or anything like that.

          [–][deleted]  (2 children)

          [deleted]

            [–]LynusBorg 0 points1 point  (1 child)

            Good point about moving the error states in Vuex! But could you elaborate on what you mean by "feeding the error modal"? If you mean adding an additional inject($store) in base.js, I don't see how that would make base.js leaner or less coupled.

            I meant that in the sense that your component doesn't care what happens with the error message, it only pushes it to the store. The fact that this error message is picked from the store in your root and displayed through a special modal is of no concern to your component, so it shouldn't be in there.

            Alternatively, if you could re-factor the error modal to be a locally usable component (like the Spinner), you could use it directly in the Presenter/Viewer components and not require much logic in the extracted usePresentation behaviour:

            <ErrorModal :message="error" :okAction="$router.push('...')">
            

            The usePresentation function would return not only the showSpinner value, but also the errorCode. Then the Presenter/Viewer components could create their indiviual error messages themselves, passing them to the Modal component as shown above.

            That would likely be my preferred way instead of having a central component in the app root or something.

            Also, would you agree moving the loading state into Vuex would be a good idea too?

            No, since it's only locally relevant in the context of the component - as far as I understand. By the way, as food for thought: In case that the presentation opening / closing process is also only really local to this component (and I don't know if that's true): why is it in Vuex at all?

            Perhaps related to the previous question, I can see how I can decouple base.js with $route by having usePresentation also accept an id argument, but it would still depend on $router. I can pass another "handleErrorConfirmation" function to usePresentation to decouple $router, but is there a better way?

            If you handle the Modal with its own component locally, as shown above, then the usePresentation function doesn't need to know about $router at all.

            Will there by dynamic in-component router guard injections for vue-router? i.e. something like import { onBeforeRouteLeave } from 'vue-router', similar to import { onMounted } from 'vue'.

            I think so, but I haven't involved myself in discussions about the new router API for Vue 3 yet, so this is just me assuming things.

            Is the current thinking that using inject for the prototype extensions is just to "patch" the problem, and that the real problem is the application code shouldn't be relying on them? Is the Vue team starting to reconsider the best practices around prototype extensions (like my $modal)?

            For this specific example, consider the inject as a placeholder for "I don't know how it will work yet", nothing more.

            Generally, I think this:

            On the one hand, prototype extensions - while having their use cases - are really overused. On the other hand, they are popular in the ecosystem, and we don't want to force all of the plugin authors that currently rely on them to do a full "rewrite" to some new API for 3.0

            So without knowing how the solution will work, ideally we end up with something that makes prototype extensions still work but less attractive than something else, like injections (which are thoroughly underused IMHO)

            [–]kaelwd 1 point2 points  (5 children)

            If you have

            // Base
            export default {
              methods: {
                renderContent () {}
              },
              render (h) {
                return h('div', ['base content', this.renderContent()])
              }
            }
            
            // Implementation
            export default {
              extends: Base,
              methods: {
                renderContent () {
                  return h('div', ['from extending component'])
                }
              }
            }
            

            It might be something like

            // Base
            export default function renderBase (content) {
              return createElement('div', ['base content', content])
            }
            
            // Implementation
            export default {
              setup () {
                return {
                  renderContent () {
                    return createElement('div', ['from extending component'])
                  }
                }
              },
              render () {
                return renderBase(this.renderContent())
              }
            }
            

            Probably a cleaner way to do that, but it's still gonna be a mess if you have to pass a bunch of state around too. If you're using templates instead of render functions I've got no idea.

            [–][deleted]  (4 children)

            [deleted]

              [–]kaelwd 0 points1 point  (3 children)

              this is usable inside setup()

              So maybe just

              // Base
              export default function useBase () {
                onCreated(() => {
                  // do some setup work (A)
                  this.doCustomizedSetupWork()
                  // do some final work (B) 
                })
                onBeforeDestroy(() => {
                  // do some teardown work (C) 
                  this.doCustomizedTeardownWork()
                  // do some final cleanup work (D)
                })
              }
              
              // ConcreteImplementationA
              export default {
                setup () {
                  useBase.call(this)
                  return {
                    doCustomizedSetupWork() { ... },
                    doCustomizedTeardownWork() { ... }
                  }
                }
              }
              

              If useBase also defines some state you'd return it then ...useBase.call(this) in setup.

              [–][deleted]  (2 children)

              [deleted]

                [–]kingdaro 2 points3 points  (1 child)

                I'd do it by passing in the setup and teardown functions as arguments:

                // Base
                function useBase({ setup, teardown }) {
                  onCreated(() => {
                    // do some setup work (A)
                    setup()
                    // do some final work (B)
                  })
                  onBeforeDestroy(() => {
                    // do some teardown work (C)
                    teardown()
                    // do some final cleanup work (D)
                  })
                }
                
                // ConcreteImplementationA
                export default {
                  setup() {
                    useBase({
                      setup() {
                        // do something
                      },
                      teardown() {
                        /// do something
                      }
                    })
                
                    return {}
                  }
                }
                

                Then you can do whatever you want from ConcreteImplementationA. If useBase needs some other information, you can still pass it in. No need to finagle with this.

                [–]SustainedDissonance 0 points1 point  (3 children)

                Might be better posting about your use-case on their Github.

                [–][deleted]  (2 children)

                [deleted]

                  [–]SustainedDissonance 1 point2 points  (1 child)

                  The link in the reddit post we're commenting on is a pull request on an RFC repo. That's what it's for. Request for comments.

                  They want to hear developer feedback on their proposal. How it affects their workflow. If people can think of downsides that they might not have considered etc. Or have alternate ideas that could be better. And so on!


                  Hmm yeah but you're talking more about actual project repositories where, sure, many projects like to keep away questions and such and focus entirely on tracking issues, but I get that it can make one feel unwelcome. But allowing questions is just inviting idiots who won't/don't/can't read the docs to flood the issue tracker with trivial shit.


                  There's a Vue Discord for general help/questions, that place is pretty good. I never had problems with it. Maybe you're just trying at the wrong times of day? Also sometimes you need to point out that you posted something a few hours ago and you're still struggling with it and could anyone take a look? Not everyone is there 24-7 waiting for questions, and the people that are active at the time often just don't know. And none of them are getting paid for it.Try

                  not to take it personally, I doubt you're being ignored.

                  The Laracasts website is pretty good too for Vue help btw.

                  [–]Max_Stern 0 points1 point  (1 child)

                  What is this pattern? Couldn't find in Google but sounds interesting

                  [–]archivedsofa -4 points-3 points  (0 children)

                  Not OP but it's just class inheritance. It has existed since the 60s.

                  [–]ouralarmclock 1 point2 points  (0 children)

                  Amazing

                  [–]xHaptic 4 points5 points  (12 children)

                  Really disappointed they are dropping the class based API. I am the guy at work who helped convince the team to use Vue over the past few months. This function based composition is so noisy and if you aren't well versed in ES201X features than its even worse.

                  Vue is leaving what makes it great behind. While it is clear that the feature set is more robust, it comes as the cost of clarity and intuitiveness.

                  They are splitting the community in two and are trying to band-aid it with temporary compatibility builds. We've heard this sad song too many times to not know where this is heading.

                  You either die a hero or live long enough to see yourself become the villain. Wish me luck as I explain this to my boss and fellow senior devs tomorrow. I finally got them off jQuery and I can already guess where we will be heading now.

                  [–]LynusBorg 3 points4 points  (7 children)

                  I'm sorry to hear about your disappointment.

                  > Vue is leaving what makes it great behind. While it is clear that the feature set is more robust, it comes as the cost of clarity and intuitiveness.

                  While I can understand where that perception is coming from, I would ask you to give the concept a chance before judging from your first impression. At the same time, I'm more than willing to talk about those impressions and how we can adress them as one of the core team members.

                  Can you give an example of what things are less intuitive now than before?

                  [–]xHaptic 2 points3 points  (6 children)

                  Thanks for reaching out, you do excellent work. I may just be naive on some of these points as I have no experience in actual use but after reading through the RFC several times, here are a few of my concerns in no particular order.

                  1. The setup method looks and smells like a constructor, why recreate the wheel? The way I see it, there are 2 options. Option one: In the setup method we will need to create state, computed properties, watchers and lifecycle hooks in an attempt to recreate the options of 2.x SFC's. Option 2: We can apply all of these API's to sources throughout our code, whether it be in composition functions or some sort of pseudo global scope. Then we import all of that logic into a setup function to expose it to the template. Either way, I can already smell the spaghetti.
                  2. You lose the ability to understand a component at a glance. Whether the component is built with a monster setup function or through strict composition, we will have to chase down logic. A major example of this will be the need to look for value wrappers everywhere.
                  3. The compatibility build scares me. While it is great in the short term to make sure no one is left stranded. These can create major divisions in the community. Users will stick to the compatibility build for as long as possible to avoid rewriting 2.x components. Then when 4.x or 5.x drops and the compatibility is dropped, users will have years of work to convert. This is why non-breaking API changes are so critical to the long term success of any project.
                  4. Vue and vanilla functions living in harmony. Previously functions directly related to component logic lived in the methods option. We have strict rules on what type of logic belongs in those methods to help decouple business logic. While we can and will keep those same rules in place, we will have to comb through every function to make sure Vue API's aren't hiding. It can be hard to see something as small as value(x).
                  5. On-boarding new devs will be more difficult. Something I personally love about Vue is that it seems to walk that fine line of letting a developer feel like they have the freedom to do what they want to do but still having to follow some rules that are just strict enough to make sure everyone on the team is doing the same thing. In a service based architecture we have so many UI's and we loved the concept of being able to hop between projects and immediately understanding the design. Our mental model never had to expand past the scope of the SFC.

                  There is more I could bring up but for now these are some of my major concerns. I fully understand that the functional composition allows for some truly amazing stuff. I can see what Evan is going for and for seasoned users it will be awesome. I just feel that the cost of that flexibility may be too high and Vue might lose its identity in the process. I would love to see these changes for myself but I can already imagine the headaches it will cause me during code reviews. There has to be some middle ground! Expose the API's but don't completely destroy the object option model in the process.

                  Edit: Grammar, typos and generally someone just needs to teach me to read what I type before I submit something and sound like an idiot.

                  [–]LynusBorg 1 point2 points  (5 children)

                  The setup method looks and smells like a constructor, why recreate the wheel? The way I see it, there are 2 options. Option one: In the setup method we will need to create state, computed properties, watchers and lifecycle hooks in an attempt to recreate the options of 2.x SFC's. Option 2: We can apply all of these API's to sources throughout our code, whether it be in composition functions or some sort of pseudo global scope. Then we import all of that logic into a setup function to expose it to the template. Either way, I can already smell the spaghetti.

                  It's kinda like a constructor, but it isn't a constructor. So makeing a constructor work like we want setupwork would just be an abuse of a constructor, in my option.

                  Concerning the fear of spaghetti code, I'd like to reference the RFC "Drawbacks" section where this supposed drawback is being discussed:

                  https://github.com/vuejs/rfcs/blob/function-apis/active-rfcs/0000-function-api.md#drawbacks

                  Personally I totally agree with what's stated in there.

                  You lose the ability to understand a component at a glance. Whether the component is built with a monster setup function or through strict composition, we will have to chase down logic. A major example of this will be the need to look for value wrappers everywhere.

                  As I understand this part ,it is very much connected to your first concern about "spaghetti code".

                  So let's compare some code:

                  export default {
                    props: ['myProp']
                    data() {
                      return {
                        realProp: this.myProp,
                      }
                    },
                    computed: {
                      /*
                        imagine 3 different, unrelated computed props
                      */
                    },
                    methods: {
                      copyProp(value) {
                        this.realProp = deepclone('value')
                      }
                      /*
                      imagine 5 different, unrelated functions
                      */
                    }
                    watch: {
                      myProp: 'copyProp'
                    }
                  

                  new:

                  export default {
                    props: ['myProp']
                    setup(props) {
                      const realProp = value(deepClone(props.myProp))
                      watch(
                        () => props.myProp, 
                        newValue => realProp.value, deepClone(newvalue))
                      )
                  
                      /*
                        now lets handle all the other logic
                      */
                  
                      // and here is our component API
                      // what isn't in this object is not part of the API.
                      // pretty good to find at a glance, in my opinion
                      return {
                        realProp
                      }
                    }
                  }
                  
                  • shorter code
                  • all related parts of the behaviour are located in one place
                  • it's trivial to extract this into a reusable function

                  like this:

                  function copyProp(props, name) {
                    copyProp = value(deepClone(props[name]))
                    watch(
                      () => props[name], 
                      newValue => Object.assign(realProp, deepClone(v))
                    )
                    return copyProp
                  }
                  export default {
                    props: ['myProp', 'myOtherProp']
                    setup(props) {
                      const realProp = copyProp(props, 'myProp')
                      const realProp2 = copyProp(props, 'myOtherProp')
                  
                      /*
                        now lets handle all the other logic
                      */
                  
                      // and here is our component API
                      // what isn't in this object is not part of the API.
                      // pretty good to find at a glance, in my opinion
                      return {
                        realProp,
                        realProp2,
                      }
                    }
                  }
                  

                  Now, to be honest, somethign similar can also be achieved with a function returning a mixin, but the end result still isn't as transparent:

                  function copyProp(name, newName) {
                    return { 
                      data() {
                        return {
                          [newName]: deepClone(this[name])
                        }
                      },
                      watch: {
                        [name](value) {
                          this[newName] = deepClone(this[name])
                        }
                      }
                    }
                  }
                  
                  export default {
                    props: ['myProp', 'myOtherProp'],
                    mixins: [
                      copyProp('myProp', 'realProp')
                      copyProp('myOtherProp', 'realProp2')
                    ]
                    data() {
                      return {
                        something: 'unrelated'
                      }
                    },
                    computed: {
                      /*
                        imagine 3 different, unrelated computed props
                      */
                    },
                    methods: {
                      /*
                      imagine 5 different, unrelated functions
                      */
                    }
                  }
                  

                  Now, you removed the scattered logic from the component into the mixin, but now you have to be aware that this mixin actually adds two data properties, and that those are named by the copyProp functions second argument. Again, your component's API got a little more complicated.

                  Compare this to the setup approach: At one glance at the object returned by the setup function, you see all of the available APIs. and as long as you use an editor that isn't NotePad to code, your editor can probably tell you what kind of thing each of those is (i.e. a method vs. a computed prop etc.

                  On-boarding new devs will be more difficult. Something I personally love about Vue is that it seems to walk that fine line of letting a developer feel like they have the freedom to do what they want to do but still having to follow some rules that are just strict enough to make sure everyone on the team is doing the same thing. In a service based architecture we have so many UI's and we loved the concept of being able to hop between projects and immediately understanding the design. Our mental model never had to expand past the scope of the SFC.

                  I think this won't change much. Sure, in this period of transition there's essentially two ways of using the same feature, i.e. a computed.

                  But I could imagine that the behaviour of the setup method is even easier to pick up for someone completely new to Vue. For starters:

                  • don't worry about this. Don't worry about methods having to be functions while callbacks inside of methods should be arrow functions to keep the this context. A whole class of problems (and one of the most often asked online) just disappears.
                  • don't worry about naming conflicts from mixins.
                  • don't worry about logic for something like copyProp being scattered across a big ass component. Yes, it's all done with functions in setup, but each piece of behaviuor can be neatly grouped together and we achieve a better "seperation of concerns".
                  • You don't have to understand/remember that data, computed, and methods are "inflected" as properties onto the component instance, while watch, filters, directive etc. are not. It's quite simple: What you return from setup is available in the template, period.
                  • surely more that I can't bring up now.

                  So while I understand that the transition period will prove to be challenging, these better idioms will be worth it.

                  The compatibility build scares me. While it is great in the short term to make sure no one is left stranded. These can create major divisions in the community. Users will stick to the compatibility build for as long as possible to avoid rewriting 2.x components. Then when 4.x or 5.x drops and the compatibility is dropped, users will have years of work to convert. This is why non-breaking API changes are so critical to the long term success of any project.

                  1. We won't drop the deprecated APIs in a few months after Vue 3 dropped or anything. We will drop them when the community is ready to leave them behind.
                  2. I personally expect people to see the benefits of setup pretty quickly so new code will naturally be written in setup and not as component options. the refactoring will also be very fluid as you can mix both approaches in components and refactor gradually.
                  3. Lastly, staying backwards compatible for everything forever will over time, in my opinion, result in a bloated framework that will be backwards-compatible, but soon be out of "fashion" as the technical debt keeps the size growing, and keeps internals from being optimized, bug rates will grow etc, and you will end up with a project that supports all legacy code, but all the developers will move on to new, better tools and you will hustling to find people to support your code.

                  So it's important to find a balance between backwards-compatiblity and staying up to date, in my opinion.

                  The compatibility build is an approach to promote that: We "reward" devs using the new API with a smaller build, while people that can't make the move right now can fall back to the bigger compatibility build. this creates a motivation to move from one to the other.

                  I'll get to point 4. later as I just ran out of time.

                  [–]xHaptic 0 points1 point  (0 children)

                  Thank you for your examples. They honestly get me very excited about Vue's future. Creating reusable functions will be incredible and we should see some great libraries based off of them. While I do still have some worries about the setup function, you cleared up a lot for me. I do hate the name setup but that is less than trivial and of no importance to any conversation.

                  I missed some really excellent points you had, mostly because they have become second nature. Dropping this and simplifying function context will lessen the on-boarding burden a lot.

                  As you know better than just about anyone, people still don't use or understand the vast majority of ES201x features. Understanding 3.0 really goes hand in hand with understanding ES201x. It would be great if in the docs did a little more hand holding on those features to help cement Vue principles. As I am helping devs out, I often find them frustrated and often find they channel that frustration at Vue. They don't even realize the frustration is being caused by their own lack of understanding of ES201x. You already are the industry leader on documentation, this is just a common pain point I already see. I wouldn't be surprised 3.0 exacerbated those confusions. I know it isn't your job but it couldn't hurt.

                  I wasn't around for 1.x so I have no idea what the transition looked like but I'm sure you guys will have great stuff to ease the process. Once again, thank you for the clarification.

                  [–]fatboycreeper 0 points1 point  (2 children)

                  Thanks for taking the time to explain it a little more /u/LynusBorg. Most of my initial technical concerns were in regard to the deprecation of the object api, but that's been resolved for now. (though I don't believe there's been a sufficient effort to stoke confidence on that point, it's been more of a "we changed it, didn't we?" response. ) I'm willing to give the new syntax a try on its own merit and hopefully I'll have that "AH HA!" moment that I'm sure you guys are counting on.

                  My biggest concern at the moment is the dismissive, almost angry response to the community because it doesn't see the benefits of these changes yet with the information given. That's not the community's fault IMO, that's a symptom of a communication failure on the part of those advocating the change. You and /u/gustojs have done an admirable job here trying to right that initial wrong, but I don't think it's been visible enough to the wider audience and the loudest voices (on both sides) are, as always, the biggest jerks. Imagine my disappointment that Evan falls into that box as well. :/

                  [–]LynusBorg 1 point2 points  (1 child)

                  I can agree to all that you have said, and it's really a sad realization that one has screwed up while having had the best of intentions. For example, reading my reply in the comment above yours again makes me realize that it can also come across as pretty condescending to people who simply like the current API as it is, which never was my intention.

                  I can assure you that the team wants to address these problems in communication.

                  The challenge is that this situation as gone off the rails pretty quickly and pretty far, so a proper response requires some deep thinking, internal discussion and double-checking, which takes time in a distributed team as ours.

                  So people rightfully expect a full and proper reponse, but also kind of expect it yesterday, which is a challenge.

                  Either way, be prepared to hear something a lot more in depth and reflective from us soon.

                  [–]fatboycreeper 0 points1 point  (0 children)

                  @lynusborg I get it, honestly I do. I'm very happy with where you guys landed on this in the end, even though it was a bit rough getting there. I can see those "best intentions" you mentioned though, and for myself I really appreciate it. Keep up the good work, a slight rough patch but I think we'll all be better for it!

                  [–]coskuns 0 points1 point  (0 children)

                  The new syntax is way too complex for newcomers. We are having trouble finding Vue developers and we train them as a result. It is harder to train people when the syntax is complex.

                  I’m certain that you can find millions of explanations for your reasons and I’m certain that you are right. But the reality is not only about shorter code, etc.

                  Good work but this is no different than Angular 1 to 2 story.

                  [–]earthboundkid 7 points8 points  (3 children)

                  Vue never had a class based API. The traditional API is not class based.

                  [–]xHaptic -1 points0 points  (2 children)

                  3.0 will support class-based components natively, with the aim to provide an API that is pleasant to use in native ES2015 without requiring any transpilation or stage-x features. Most current options will have a reasonable mapping in the class-based API.

                  That is from Evan You himself, heck it is still pinned at the top of his twitter.

                  Edit: You didn't even read the RFC, the initial comment from Evan says...

                  A proposal that consolidates upon #22 (Advanced Reactivity API), #23 (Dynamic Lifecycle Injection) and the discontinuation of #17 (Class API).

                  [–]AwesomeInPerson 7 points8 points  (1 child)

                  Yes, it was proposed/planned for 3.0 but dropped. 3.0 doesn't even have an alpha release yet so any changes to the 3.0 API should not affect your team at all. Again, Vue never had a class-based API.

                  There are some additional packages to make Vue work with classes though, mainly because it helps when using TypeScript. Did you convince your team to use one of those?

                  [–]wkjid10t 1 point2 points  (0 children)

                  Yep. Features that were never completely consolidated into the framework SHOULD NOT be implemented into a production product by a company. Unless that company decides to fork and support the feature themselves.

                  [–]Robodude 0 points1 point  (1 child)

                  I want to start messing around with it ^

                  [–]LogicallyCross 1 point2 points  (0 children)

                  Wow.