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

Child component <slot v-if="false"> will still evaluate parent slot code #8578

Closed
bbugh opened this issue Jul 30, 2018 · 10 comments
Closed

Child component <slot v-if="false"> will still evaluate parent slot code #8578

bbugh opened this issue Jul 30, 2018 · 10 comments

Comments

@bbugh
Copy link

bbugh commented Jul 30, 2018

Version

2.5.16

Reproduction link

bbugh/v-if-slot-issue@ffd6d47

Steps to reproduce

  1. Create a child component with a conditional slot:

    <template>
      <div>
        <slot v-if="showSlot" />
      </div>
    </template>
  2. Use the slot in a parent component, using code that should not be called when v-if resolves as false:

    <conditional-slot :showSlot="false">
      {{ example.should.not.be.called }}
    </conditional-slot>
    
    data: () => ({
      example: {}
    })

Observe that slot code is called, and fails because "example" is an empty object.

A runnable demo of this is in the linked repository.

What is expected?

The behavior is expected to match v-if's behavior in other circumstances, which is that the code in the if block is not evaluated when the conditional check resolves as false.

What is actually happening?

The parent slot code is always evaluated, regardless of the value of v-if in the child component.

@afontcu
Copy link
Member

afontcu commented Jul 30, 2018

Hi! If I understand it correctly, this is not a bug.

v-if prevents the conditional block to be rendered if the condition evaluates to false, but it is not preventing Vue to parse/evaluate the slot. So, in your example, App.vue is still trying to access example.should, whether if it renders or not.

This is the compiled render function of your App component. Notice how example.should... is still being accessed.

I'd say this is expected behaviour, because that v-if prevents rendering, not parsing/evaluation (AFAIK).

Hope it helps!
😄

@bbugh
Copy link
Author

bbugh commented Jul 30, 2018

No, that is not correct. v-if does not evaluate the contents if it is falsey. You might be thinking of v-show, which does evaluate the block whether it's shown or not.

Demo:

<!-- this will work just fine because it's not evaluated -->
<div v-if="false">
  {{ example.should.not.be.called }}
</div>
<!-- this will be an error because it is evaluated -->
<div v-show="false">
  {{ example.should.not.be.called }}
</div>

@afontcu
Copy link
Member

afontcu commented Jul 30, 2018

Hi again! 👋

Now I see that in my comment I didn't state clearly that I was talking about the v-if + slots context, not every v-if situation. My bad!

However, I'd say this is an expected behaviour because "everything in the parent template is compiled in parent scope" (source). And in your parent component, example.should does not exist, no matter what.

Be that as it may, maybe the docs should reflect this edge case somewhere. Or at least a warning?

@bbugh
Copy link
Author

bbugh commented Jul 30, 2018

I am not sure why you’re commenting on my issue, I’m not asking for help, I’m reporting a bug.

@yyx990803
Copy link
Member

This is expected behavior as @afontcu has explained. Parent slot content is "rendered" into VNode eagerly before being passed down to child for slot resolution. If you want it to be lazy, use scoped slots. In the future we may make all slot content lazy by default, but not until we have the opportunity to make breaking changes (i.e. 3.0)

@aldencolerain
Copy link

@afontcu We just upgraded to Vue and started experiencing this issue. Do you happen to know if the default slot content was always eagerly loaded? We had a layout template with a v-if statement that showed a loading screen until content was loaded and were surprised to find it breaking due to this after an upgrade to 2.6.10.

@sirlancelot
Copy link

You can use the new v-slot syntax (<template #default></template>) to prevent the slot content from rendering until its needed.

v-slot documentation

@eZanmoto
Copy link

I'd just like to elaborate on @yyx990803 and @sirlancelot's answers with a bit more detail for people landing on this page looking for quick answers. Take the following code as an example (preview at https://jsfiddle.net/yd2Lpcf1/4/):

<div id="app">
  <div>
    <div v-if="!person">Loading</div>
    <div v-else>Hi {{person.name}}</div>
    
    <layout :loading="!person">
      Hi {{person.name}}
    </layout>
  </div>
</div>

<template id="layout">
  <div>
    <div v-if="loading">Loading</div>
    <slot v-else></slot>
  </div>
</template>
Vue.component('layout', {
  template: '#layout',
  props: ['loading']
});

new Vue({
	el: '#app',
  data() {
    return { person: null }
  },
  mounted: function () {
    setInterval(() => {
	    this.person = this.person ? null : {name: 'John'}
    }, 1000)
  }
});

When this is run in the fiddle linked above then the preview will be blank for a second, then "Hello John" will be shown twice. If run in its own page, then the error console will show the error Cannot read property 'name' of null, referring to the fact that person.name defined in the <layout> section was eagerly evaluated and raised an error. This error is thrown every second. This demonstrates that slots are evaluated eagerly and that the error halts the rendering of the child component and the parent component - "Loading" isn't printed in the first v-if, nor in the one in the child component. It also demonstrates that, even though the slot was eagerly evaluated originally, it is still re-rendered when the value of person changes.

Following @sirlancelot's recommendation, the only thing that needs to change is to nest an extra <template> layer inside the <layout>:

    <layout :loading="!person">
      <template #default>
        Hi {{person.name}}
      </template>
    </layout>

When the page is re-run then the preview will show "Loading" twice, for a second, and then "Hello John" will be shown twice, and these values will change every second. The error console will be blank. This demonstrates that the slot is lazily evaluated following the expected v-if logic, as expected, and the whole component is properly re-evaluated reactively, instead of just being re-rendered.

In short, when you're using a component with a conditional slot like <MyBaseComponent>...</MyBaseComponent>, simply wrap the content as @sirlancelot described (<MyBaseComponent><template #default>...</template></MyBaseComponent>), and the default slot will evaluate in the way you'd expect.

As an aside, if support for Vue 2.0 is expected to continue for much longer, I would recommend updating the documentation for slots with a section on the "Eager Evaluation" caveat and how it can be solved using the v-slot workaround.

@martianmartian
Copy link

martianmartian commented Jul 30, 2021

@yuta-san
Copy link

looks like at least vue 3.2.19 is lazy.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants