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

Hooks integration #12

Closed
yyx990803 opened this issue Oct 28, 2018 · 14 comments
Closed

Hooks integration #12

yyx990803 opened this issue Oct 28, 2018 · 14 comments

Comments

@yyx990803
Copy link
Member

yyx990803 commented Oct 28, 2018

Rationale

https://twitter.com/youyuxi/status/1056673771376050176

Hooks provides the ability to:

  • encapsulate arbitrarily complex logic in plain functions
  • does not pollute component namespace (explicit injection)
  • does not result in additional component instances like HOCs / scoped-slot components
  • superior composability, e.g. passing the state from one hook to another effect hook. This is possible by referencing fields injected by other mixins in a mixin, but that is super flaky and hooks composition is way cleaner.
  • compresses extremely well

However, it is quite different from the intuitions of idiomatic JS, and has a number of issues that can be confusing to beginners. This is why we should integrate it in a way that complements Vue's existing API, and primarily use it as a composition mechanism (replacement of mixins, HOCs and scoped-slot components).

Proposed usage

Directly usable inside class render functions (can be mixed with normal class usage):

class Counter extends Component {
  foo = 'hello'
  render() {
    const [count, setCount] = useState(0)
    return h(
      'div',
      {
        onClick: () => {
          setCount(count + 1)
        }
      },
      this.foo + ' ' + count
    )
  }
}

For template usage:

class Counter extends Component {
  static template = `
    <div @click="setCount(count + 1)">
      {{ count }}
    </div>
  `

  hooks() {
    const [count, setCount] = useState(0)
    // fields returned here will become available in templates
    return {
      count,
      setCount
    }
  }
}

In SFC w/ object syntax:

<template>
  <div @click="setCount(count + 1)">
    {{ count }}
  </div>
</template>

<script>
import { useState } from 'vue'

export default {
  hooks() {
    const [count, setCount] = useState(0)
    return {
      count,
      setCount
    }
  }
}
</script>

Note: counter is a super contrived example mainly to illustrate how the API works. A more practical example would be this useAPI custom hook, which is similar to libs like vue-promised.

Implementation Notes

Proposed usage for useState and useEffect are already implemented.

Update: Mapping w/ Vue's existing API

To ease the learning curve for Vue users, we can implement hooks that mimic Vue's current API:

export default {
  render() {
    const data = useData({
      count: 0
    })

    useWatch(() => data.count, (val, prevVal) => {
      console.log(`count is: ${val}`)
    })

    const double = useComputed(() => data.count * 2)

    useMounted(() => {
      console.log('mounted!')
    })
    useUnmounted(() => {
      console.log('unmounted!')
    })
    useUpdated(() => {
      console.log('updated!')
    })

    return [
      h('div', `count is ${data.count}`),
      h('div', `double count is ${double}`),
      h('button', { onClick: () => {
        // still got that direct mutation!
        data.count++
      }}, 'count++')
    ]
  }
}
@HerringtonDarkholme
Copy link
Member

Even as a traditional class fan, I believe hooks are superior and will prevail frontend development.
Great to see it integrated in Vue-next!
My only concern is that should we mix hook with normal class? They are two different mental model and it might be difficult for users to switch between...

@posva
Copy link
Member

posva commented Oct 29, 2018

Funny thing, the latest version I released for vue-promised uses named imports so it could also export a hook version.

The pattern seems powerful but as you said it's hard for beginners so we have to be careful not to replace existing patterns that are much easier to grasp for newcomers because that's one of Vue good points

About alternative syntaxes, something more magical could be useful, but I'm not even sure about that. I personally don't like:

<template>
  <div @click="setCount(count + 1)">
    {{ count }}
  </div>
</template>

<script>
import { useState } from 'vue'

export default {
  hooks: [
	useState(0, ['count', 'setCount'])
  ]
}
</script>

The thing that tickles me is that hooks are clearly oriented to render functions.

What makes this pattern so good in compression?

@yyx990803
Copy link
Member Author

yyx990803 commented Oct 29, 2018

@HerringtonDarkholme Hooks are stateful under the hood. A functional component with hooks is no longer a stateless functional component, it is internally represented as a stateful instance. It actually works fine in a class. On the other hand, not making it available in the idiomatic API makes it useless to a large group of Vue users.

@posva options like that misses a critical feature of hooks: that they can pass values between each other in the same function scope. I thought about making mixins explicitly expose properties, but hooks is still better.

Hooks compress better also due to the function scope - every variable inside can be shortened to single letters.

@chrisvfritz
Copy link
Contributor

Hook call order

@yyx990803 I agree with pretty much everything you said on Twitter. Another education concern I have with hooks is that they must be called in the same order every time the render function is run, which isn't intuitive unless you understand their magic. To me, this makes them almost unusable outside of an ESLint environment, because we could only prevent people from constantly shooting themselves in the foot with a rule to warn/train them.

count/setCount vs proxied getters and setters

I'm sure there are some aspects of hooks I'm still not understanding, so forgive me if this is a naive question, but why would we need to return a setCount, rather than just count with a proxied getter and setter?

@yyx990803
Copy link
Member Author

@chrisvfritz yeah, I think we would introduce hooks as an advanced feature for code reuse.

Re count/setCount: you are right, if you create an object with useState(), you can still directly mutate it and it will work. I added a new paragraph showing how we can provide hooks that maps Vue's current API (and I think it's promising!)

We can provide hooks like useWatch and useComputed, although with one extra rule: you need to access state as a property (i.e. always with a dot) inside the getter of computed and watch so you can't use destructuring when you create the state.

@chrisvfritz
Copy link
Contributor

I'd also like to propose that we always call these "render hooks" to avoid confusion with lifecycle hooks, then start using some other term for lifecycle hooks starting Vue 3 (e.g. "lifecycle functions"). If a Vue user ever has to say the words "lifecycle hook hook" or even "lifecycle hook render hook", I'll cry a little. 😄

@HerringtonDarkholme
Copy link
Member

HerringtonDarkholme commented Oct 30, 2018

Mapping current API to hooks looks fantastic 😍 It feels like authentic Vue in hooks and type checks better! (lest lifecycle renaming fuss 😄 ). I'll try with Vue2 to see if it works well.

@posva About minimizing, if we cannot rename properties on object in most compressor, but hooks expose themselves as local variables and plain function. So minimizers can take advantage of it.

@yyx990803
Copy link
Member Author

Updated vue-hooks to support Vue-style hooks + hooks() in 2.x: https://github.com/yyx990803/vue-hooks#usage-in-normal-vue-components

@Jinjiang
Copy link
Member

Could there be a setter for useComputed? And useData seems only accept an object or array which is a little constraint for that I think.

@anthonygore
Copy link

I'd also like to propose that we always call these "render hooks" to avoid confusion with lifecycle hooks, then start using some other term for lifecycle hooks starting Vue 3 (e.g. "lifecycle functions"). If a Vue user ever has to say the words "lifecycle hook hook" or even "lifecycle hook render hook", I'll cry a little. 😄

Yeah, avoiding that ambiguity is important. In fact, "hooks" in web development usually refers to custom code being called after some action or event (Git hooks, WordPress hooks, Vue's lifecycle hooks etc). To me, the word "injection" makes more sense for this feature than "hook".

@chrisvfritz
Copy link
Contributor

Could there be a setter for useComputed?

@Jinjiang I think it would make sense for useComputed to work the same as current computed properties, where they can accept a function or object with get and set methods:

const double = useComputed({
  get: () => data.count * 2
  set: newValue => { data.count = newValue / 2 }
})

What do you think?

useData seems only accept an object or array which is a little constraint for that I think.

I think the idea is a little different from what React is doing. Instead of a new useState for each individual piece of state, I think it would be a good practice to have only a single useData per render function. This way, it'll remain easy to see all the component's internal state at a glance (just like with the object- and class-based syntaxes). Does that make sense?

"hooks" in web development usually refers to custom code being called after some action or event (Git hooks, WordPress hooks, Vue's lifecycle hooks etc). To me, the word "injection" makes more sense for this feature than "hook".

@anthonygore I absolutely agree with you that "hooks" is a confusion name for this feature. Unfortunately, the React team never asked us before introducing the pattern. 😅 And I worry calling it anything else at this point would just cause more confusion, since we have so many developers coming from React. Does that make sense?

@phanan
Copy link
Member

phanan commented Nov 3, 2018

To me, the word "injection" makes more sense for this feature than "hook".

@anthonygore @chrisvfritz Actually, "injection," or "inject," has already been used for a very different feature :)

useData seems only accept an object or array which is a little constraint for that I think.

@Jinjiang I'm a bit lost here – why is it a constraint? The only other option for data is a function, which is, as far as my understanding goes, not relevant or necessary anymore in a hook context as the state won't be shared across components. What am I missing?

@Jinjiang
Copy link
Member

useData seems only (to) accept an object or array which is a little constraint for that I think.

@Jinjiang I'm a bit lost here – why is it a constraint? The only other option for data is a function, which is, as far as my understanding goes, not relevant or necessary anymore in a hook context as the state won't be shared across components. What am I missing?

@phanan I mean useData() cannot accept a primitive value like string, number or boolean. It seems like data() in Vue but not like setState(0) in React or in the new design above which accepts a primitive value directly.

And via:

https://github.com/vuejs/vue-next/blob/774cce324d7b12b3db82b6f18e911bea36f9424a/packages/runtime-core/src/experimental/hooks.ts#L153

I think useData() is neither data() in Vue nor useState() in React. So that's all my concern.

Thanks.

@yyx990803
Copy link
Member Author

Closing in favor of the newer RFCs (vuejs/rfcs#22, vuejs/rfcs#23)

@github-actions github-actions bot locked and limited conversation to collaborators Nov 17, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants