composition-api: context.slots is empty in setup()

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

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Comments: 16 (6 by maintainers)

Most upvoted comments

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

Getting slots from render function arguments should work

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

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)

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.