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

context.slots is empty in setup() #84

Closed
tiepnguyen opened this issue Aug 25, 2019 · 16 comments
Closed

context.slots is empty in setup() #84

tiepnguyen opened this issue Aug 25, 2019 · 16 comments
Labels
documentation Improvements or additions to documentation

Comments

@tiepnguyen
Copy link

I'm not sure if this is an issue or I'm doing something wrong, but currently I got "context.slots" an empty object in setup(), although it's available in lifecycle hook functions

import { onMounted, createElement as h } from '@vue/composition-api'

export default {
  name: 'HelloWorld',
  props: {
    msg: String
  },
  setup(props, context) {
    console.log(context.slots) // EMPTY OBJECT HERE

    onMounted(() => {
      console.log(context.slots) // SLOTS AVAILABLE HERE
    })

    return () => <div class="hello">{slots.default}</div>
  }
}

The thing is, I need {slots.default} for render function / JSX inside setup(), so I can't use this.$slots.default either since "this" is not available in setup().

If I try to destruct context to { slots } then it even empty inside onMounted function

import { onMounted, createElement as h } from '@vue/composition-api'

export default {
  name: 'HelloWorld',
  props: {
    msg: String
  },
  setup(props, { slots }) {
    console.log(slots) // EMPTY OBJECT HERE

    onMounted(() => {
      console.log(slots) // ALSO EMPTY HERE
    })

    return () => <div class="hello">{slots.default}</div>
  }
}
@PatrykWalach
Copy link

Getting slots from render function arguments should work

export default createComponent({
  setup: () => (_, { slots }) => <div>{slots.default()}</div>
})

@tiepnguyen
Copy link
Author

@PatrykWalach yes it works, thanks man.
So... this is composition-api plugin issue or it's a part of Vue 3.0 rfs?

@liximomo
Copy link
Member

liximomo commented Aug 26, 2019

The context.slots is a proxy of vm.$scopedSlots and $scopedSlots is resolved in render phase. I would say that the behavior is expected. Are there any use cases that the slots must be accessed out of render() function?

@liximomo liximomo added the documentation Improvements or additions to documentation label Aug 26, 2019
@tiepnguyen
Copy link
Author

@liximomo thanks for explanation, that totally makes sense and reasonable, however I'm still wondering if this is a limitation of composition-api plugin (due to the lack of Vue 2 api?) or it's a part of Vue 3.0 composition.

For Vue 3.0 official if I can destruct { slots } from setup context already, but cannot use in setup function, and have to do it again in render function, then it's a bit confuse to me. Btw, I checked render function api change again and see that for vue 3, render function 2nd parameter should be slots, not context, a bit more confuse here 😅

In my case, the reason I need slots out of render() is becus I don't wanna compose the logic inside render function (I only put jsx there mainly for markup)

I simplified the situation here...

import { computed, createElement as h } from '@vue/composition-api'

export default {
  name: 'ListView',
  props: {
    data: { type: Array, default: () => [] }
  },
  setup: (props, { slots }) => {
    // simplified composition logics
    const renderItem = (item) => {
      return slots.default && slots.default(item) || <li>{item}</li>
    }

    const listItems = data.map(renderItem)

   // Simplified jsx to avoid confusion
    return () => (
      <ul>
        {listItems}
      </ul>
    )
  }
}

At the moment since I don't have slots access out of render() so I have to either:

  1. move all logic that need slots into render(), which makes it verbose and harder to understand
  2. pass the slots parameter all the way from render() to any function that need (then what's the point of setup() context.slots?)

@liximomo
Copy link
Member

liximomo commented Aug 26, 2019

move all logic that need slots into render(), which makes it verbose and harder to understand

You can create a function, then call it in the render function. And it's not safe to call slots out of render function, you would lose the reactivity.

Based on your example:

import { computed, createElement as h } from '@vue/composition-api'

export default {
  name: 'ListView',
  props: {
    data: { type: Array, default: () => [] }
  },
  setup: (props, { slots }) => {
    // simplified composition logics
    const renderItem = (item) => {
      return slots.default && slots.default(item) || <li>{item}</li>
    }
    const renderItems = () => data.map(renderItem);

   // Simplified jsx to avoid confusion
    return () => (
      <ul>
        {renderItems()} 
      </ul>
    )
  }
}

@tiepnguyen
Copy link
Author

@liximomo thanks for your reply, I tried again, this doesn't work

import { createElement as h } from '@vue/composition-api'

export default {
  name: 'ListView',
  props: {
    data: { type: Array, default: () => [] }
  },
  setup: (props, { slots }) => {
    const renderItems = () => data.map(item => {
      return slots.default && slots.default(item) || <li>{item}</li>
    });

    return () => (
      <ul>
        {renderItems()} 
      </ul>
    )
  }
}

But this works

import { createElement as h } from '@vue/composition-api'

export default {
  name: 'ListView',
  props: {
    data: { type: Array, default: () => [] }
  },
  setup: (props, { slots }) => {
    const renderItems = (slots) => data.map((item => {
      return slots.default && slots.default(item) || <li>{item}</li>
    })

    return (_, { slots }) => (
      <ul>
        {renderItems(slots)} 
      </ul>
    )
  }
}

Like I said earlier

pass the slots parameter all the way from render() to any function that need

So what's the point of setup() context.slots, it must be there for some reason, right?

@liximomo
Copy link
Member

Looks like Vue 2 created a new object for $scopedSlots in every render. @yyx990803 What do you think?

@hiendv
Copy link

hiendv commented Aug 26, 2019

Disclaimer: This is my own perspective of the issue as a developer who works with $slots and its friends a lot.

So from my understanding, there are three questions here:

  1. Why context.slots is empty at first?
  2. What is the purpose of context.slots in setup if you can't use it?
  3. How to separate rendering & the setup stuff?

Well, as @liximomo already stated, the context.slots is a proxy of vm.$scopedSlots which means that the problem you are facing is not actually relevant to composition-api. Why not? Because you would have the same problem if you write the component without @vue/composition-api.

You are trying to use $scopedSlots but they are not reactive. Imagine you write the component in the common way, the value of this.$scopedSlots before rendering would be the same. So how do you solve your problem in question 3? I will come back to this later.

  1. The context.slots is empty because it's not loaded yet.
  2. The context.slots is the reflection of vm.$scopedSlots. They are in the context because they are supposed to. They are not for render only, don't get this wrong. There are usecases and yes, you can definitely use it.

Nuff said, how to separate rendering & the setup stuff?
Try to solve this instead: How to separate vm.$slots related stuff from the rendering, they are not reactive right? Define a "ghost" copy of $slots in data and take the advantage of reactivity with it.

Let's say I want to have a component Foobar which renders attributes of slots, but only for <div/> slots. (I know, it sounds dull 🤣)

<foobar foo="bar">
  <div attr="val"/>
  <div foo="bar"/>
  <p title="not you"/>
</foobar>

<!-- expected:
  { "attr": "val" }
  { "foo": "bar" }
-->

The "normal" way

<template>
  <div class="items">
    <div v-for="(item, index) in items" :key="index">
      {{ item }}
    </div>
  </div>
</template>
<script>
export default {
  data () {
    return {
      slots: []
    }
  },
  computed: {
    items () {
      return this.slots
        .filter(vnode => {
          return vnode.tag === 'div'
        })
        .map(vnode => {
          return vnode.data.attrs
        })
    }
  },
  created () {
    this.slots = this.$slots.default || []
  }
}
</script>

The composition way

<template>
  <div class="items">
    <div v-for="(item, index) in state.items" :key="index">
      {{ item }}
    </div>
  </div>
</template>
<script>
import { reactive, computed, onMounted } from '@vue/composition-api'

export default {
  setup (props, context) {
    const state = reactive({
      slots: [],
      items: computed(() => {
        return state.slots
          .filter(vnode => {
            return vnode.tag === 'div'
          })
          .map(vnode => {
            return vnode.data.attrs
          })
      })
    })

    onMounted(() => {
      // onCreated is deprecated https://github.com/vuejs/composition-api/blob/9d8855a4a293321075c93b15d631a43681c2605b/src/apis/lifecycle.ts#L30
      state.slots = context.slots.default() || []
    })

    return {
      state
    }
  }
}

</script>

As you can see, context.slots is a reflection of vm.$slots ($scopedSlots). It should be in the context object.
That is my two cents.

@hiendv
Copy link

hiendv commented Aug 26, 2019

I was trying to solve the reactivity problem but it doesn't fit right with the original issue from @tiepnguyen. As you can see, the slots can be define like this and works just fine but there is no way to get it done with @vue/composition-api

data () {
  return {
    slots: this.$slots.default
  }
}

So I decided to take a close look at the two libraries and did notice the difference between $slots and $scopedSlots initialization here.
https://github.com/vuejs/vue/blob/399b53661b167e678e1c740ce788ff6699096734/src/core/instance/render.js#L25-L26

https://github.com/vuejs/vue/blob/399b53661b167e678e1c740ce788ff6699096734/src/core/instance/render.js#L73-L79

As you can see, $scopedSlots is not initiated until _render because it requires parent scopedSlots data.

Despite the documentation:

All $slots are now also exposed on $scopedSlots as functions. If you work with render functions, it is now recommended to always access slots via $scopedSlots, whether they currently use a scope or not. This will not only make future refactors to add a scope simpler, but also ease your eventual migration to Vue 3, where all slots will be functions.

I think $slots should not be aliased to $scopedSlots unless $slots is deprecated for Vue 3. If it is, we can only interact with $scopedSlots after rendering.

So, summary?

  • This issue can be solved if @vue/composition-api offers both $slots and $scopedSlots in the context although it goes against the migration to Vue 3.
  • The reactivity problem remains as my above comments. Shadow copy is needed when you work with $slots because they are not reactive.

@tiepnguyen
Copy link
Author

tiepnguyen commented Aug 26, 2019

@hiendv thanks for your big effort and detail explanation, your solution of shadow copy slots to make it reactive then make use of onMounted hook to give it value, seems to be legit, but a bit too much for me 😅. Besides, it only works if I pass the whole context as setup() 2nd parameter, since destructing to { slots } makes it lost reactivity.

So at the moment I'll just either

  1. pass slots from render function to my logic composition, or
  2. move all logics that need slots into render function then

@yyx990803
Copy link
Member

yyx990803 commented Aug 28, 2019

Per spec:

  • the context.slots object should be an object that proxies to the actual slots object
  • the actual slots object is replaced on each render
  • for context.slots to work with destructuring, instead of proxying context.slots access, we need to proxy each property access on context.slots.
    • In current 3.0 implementation this is done with native Proxy.
    • In this plugin, we will need to:
      1. resolve initial $scopedSlots before setup() is called (this may require changes in Vue core 2.x itself)
      2. Proxy each existing slot on context.slots
      3. Before each render, check if slot keys have changed (e.g. slots changed from { foo, bar } -> { bar, baz }. This should be rare)

@hiendv
Copy link

hiendv commented Aug 29, 2019

@yyx990803 If we recommend people "to always access slots via $scopedSlots, whether they currently use a scope or not", I think we should initial $scopedSlots before setup().
Regarding Vue core, are you open for the change? Is the Vue 3 slot resolution in initRender or _render prototype?

@yyx990803
Copy link
Member

@hiendv I discussed with @liximomo yesterday and we are going to duplicate $scopedSlots resolution logic in this plugin instead of changing Vue 2.x core, because changing the latter will create a hard Vue core version requirement for this plugin.

In Vue 3 - yes, slots are resolved before setup.

@hiendv
Copy link

hiendv commented Aug 31, 2019

@yyx990803 Thanks, Evan. Just to be clear, for this plugin, we will keep the references to slots and scoped slots separately right? Or we keep unifying them, let scopedSlots have the initial value of slots?

@hiendv
Copy link

hiendv commented Aug 31, 2019

Nvm, I saw the commit. 3b8cfc2

@liximomo
Copy link
Member

liximomo commented Sep 2, 2019

@liximomo liximomo closed this as completed Sep 2, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Improvements or additions to documentation
Projects
None yet
Development

No branches or pull requests

5 participants