Vue-next: Hooks integration

Created on 29 Oct 2018  ยท  14Comments  ยท  Source: vuejs/vue-next

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++')
    ]
  }
}

Most helpful comment

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

All 14 comments

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...

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?

@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.

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?

@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.

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. ๐Ÿ˜„

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.

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

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.

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".

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?

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?

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.

Closing in favor of the newer RFCs (https://github.com/vuejs/rfcs/pull/22, https://github.com/vuejs/rfcs/pull/23)

Was this page helpful?
0 / 5 - 0 ratings

Related issues

chrisvfritz picture chrisvfritz  ยท  4Comments

doman412 picture doman412  ยท  3Comments

NMFES picture NMFES  ยท  3Comments

ConradSollitt picture ConradSollitt  ยท  4Comments

anandkumarram picture anandkumarram  ยท  4Comments