nuxt / nuxt

The Intuitive Vue Framework.
https://nuxt.com
MIT License
54.53k stars 4.99k forks source link

Static generation with initial negative v-if or condition in components renders out the component #8193

Closed davydnorris closed 3 years ago

davydnorris commented 4 years ago

Versions

Reproduction

This component illustrates the issue - it's using Vuetify for formatting but basically just presents a message at the bottom of the screen regarding cookies

myGDPRNote.vue

<template>
  <v-snackbar
    :value="showAccepted"
    :timeout="-1"
    bottom
  >
    This website uses cookies to ensure you get the best experience.
    <a
      class="text-decoration-underline"
      @click="dialog=true"
    >
      Learn more
    </a>
    <v-dialog
      v-model="dialog"
      width="600"
    >
      <v-card>
        <v-card-actions>
          <v-spacer />
          <v-btn
            @click="dialog = false"
          >
            OK
          </v-btn>
        </v-card-actions>
      </v-card>
    </v-dialog>
    <template v-slot:action="{ attrs }">
      <v-btn
        v-bind="attrs"
        @click="acceptCookies"
      >
        Got it!
      </v-btn>
    </template>
  </v-snackbar>
</template>

<script>
export default {
  data: () => ({
    dialog: false,
    showAccepted: false   // this is the crucial setting
  }),
  created () {
    if (process.browser) {
      this.showAccepted = !localStorage.acceptCookies;
    }
  },
  methods: {
    acceptCookies () {
      this.showAccepted = false;
      if (process.browser) {
        localStorage.acceptCookies = true;
      }
    }
  }
};
</script>

This is the resulting HTML in the browser:

<div class="v-snack v-snack--active v-snack--bottom v-snack--has-background" style="padding-bottom:0px;padding-top:108px;">
  <div class="v-snack__wrapper v-sheet theme--dark" style="display:none;">
    <div role="status" aria-live="polite" class="v-snack__content">
      This website uses cookies to ensure you get the best experience.
      <a class="text-decoration-underline">
        Learn more
      </a>
      <div role="dialog" class="v-dialog__container">
        <!---->
      </div>
    </div>
    <div class="v-snack__action ">
      <button type="button" class="v-btn v-btn--contained theme--dark v-size--default v-snack__btn">
        <span class="v-btn__content">
          Got it!
        </span>
      </button>
    </div>
  </div>
</div>

Steps to reproduce

When this component is included in a project, it runs fine in dev mode and universal mode, but when a full static site is generated the note never appears

What is Expected?

The note should appear as a component/element in the code but not be displayed by default. The created () method should then set the showAccepted property based on the local storage and show the note. Clicking the Got it! button should hide the note and set local storage. Subsequent refreshes of the page should not show the note until local storage is cleared

What is actually happening?

The created () method is correctly setting the property (tested with console output before and after) but the note never appears. It appears that the code to change the display from none is not being executed when the site has been statically generated.

If I set the initial value of showAccepted to true, the note works correctly, however the note appears briefly and then disappears on the first page load, which seems to show that the actual created () and display logic is being run after the page has been rendered

I also initially had the contents of the created () method as the fetch () method. This worked in dev and universal mode but static generation removed the fetch () method completely, which is really baffling. I thought it could be used to get dynamic data???

Additionally I originally had this banner as a v-footer with a v-if="showAccepted" instead and that failed completely. The footer ended up either entirely missing if the initial value of showAccepted was false, or permanently shown - changes to showAccepted in the code were completely ignored

manniL commented 4 years ago

Hey @davydnorris šŸ‘‹šŸ» Thanks a lot for the detailed report!

Any errors in the console?

I suggest to try the following: Move the content of created into beforeMount or mounted as these functions will run only on the client-side and don't need another check for process.browser. Also, as the GDPRNotice component is using localstorage, you might not need to render it at all on the server side as you can't access localstorage there. Thus, you might want to wrap the component in a <ClientOnly> tag.

davydnorris commented 4 years ago

@manniL - in the case when I tried to use v-if with the v-footer, I got a virtual DOM mismatch error when the component was hidden, but this was in both static and dev modes so I don't think it's related.

I tried a number of things including wrapping the footer in a <div> but couldn't get the error to disappear. I thought it was maybe because I had two footers (a standard one attached to the end of each page and the fixed position one that disappeared) but this is allowed apparently. In dev mode I could use the Vue console tools and could see the top level footer component in the vDOM but no matching element in the HTML so I think it's a Veutify issue or maybe even a Vue issue?

In the case of the above code used in the report, there's no errors at all.

Thanks also for the tips but this was always going to be a statically generated site deployed on an Ngnix CloudFoundry buildpack. Apart from the cookies banner, there are only a few dynamic bits, all related to the site's blog, which I've built using nuxt/content:

As mentioned above, and as highlighted in my other feature request, chasing down this bug revealed a lot of things about static generation and the way nuxt currently works that was not clear in the documentation and may actually be a bug as well:

Given that created () causes visual glitches in the above code, I would assume beforeMount () and mounted () would also be problematic - also the cookies banner is added to the default.vue layout and is displayed on every page until it's acknowledged, then it's always hidden. Adding the code to beforeMount () or mounted () would mean that it gets executed on every page display instead of just the first time the banner component is created

manniL commented 4 years ago

in the case when I tried to use v-if with the v-footer, I got a virtual DOM mismatch error when the component was hidden, but this was in both static and dev modes so I don't think it's related.

v-if + checking for browser/server almost always creates a hydration error because the server HTML is different from what the client expects.

As mentioned above, and as highlighted in my other feature request, chasing down this bug revealed a lot of things about static generation and the way nuxt currently works that was not clear in the documentation and may actually be a bug as well

Would highly appreciate issues in the docs repo or PRs on the docs directly šŸ‘šŸ»

fetch () and asyncData () calls get generated out in static sites so can't be used for dynamic data. This really got me and I wonder if this isn't a bug. I would expect these to be retained as javascript in a statically generated site and, based on the nuxt documentation, were where I had put my code to get the dynamic blog pieces

Yes, both methods are intentionally triggered only during generation and the results are saved in payload files. This has been introduced with the full static mode (see this post). You can also disable this behavior per page component if wanted.

created (), which should be the first call in the lifecycle, and which the documentation shows as being complete before any render or display is causing visual effects after the page has been displayed, which is very weird.

Created is called on both, server- and client-side and must be treated with care (e.g. by checking for client/server as you did) as it can be a pitfall for components. Important: When executing in created only on client side, visuals from the server-side rendering are already present. So again, this is intended.

From my experience you have basically two options for your GDPR notice:

  1. Show on server no matter what the client selected (as no access to localstorage) and hide it on client-side if the client pushed the button
  2. Show it only on client-side and fade it in, if the client has not clicked the GDPR notice yet.

PS: I have a cookie banner with similar logic on my page (open source).

davydnorris commented 4 years ago

Thanks for this - one thing to reiterate, this site is only ever going to be full static so any discussion about server side is not relevant here, but thanks for the great explanations.

manniL commented 4 years ago

Thanks for this - one thing to reiterate, this site is only ever going to be full static so any discussion about server side is not relevant here, but thanks for the great explanations.

I am afraid that is not the case. Static Site Generation is SSR but only at build time. So process.server will be true during site generation. So it is important šŸ˜‹

Glad I was able to help šŸ‘

davydnorris commented 4 years ago

No I totally understand that, but the key thing that I'm trying to understand is how to make sure any dynamic elements of the generated site work, or even if they will work. I guess what is needed in the workflow discussions is a description of what happens during generation, and then what happens when the page is fetched at deployment.

In my case, almost all of the site is static except for some numbers on preview cards and comment threads on a blog. It would be great to enhance the documentation so that it's clear where to add JavaScript that will fetch this content when the static site is deployed, or even if this is possible at all after full static generation - there is obviously some JavaScript stuff going on but it's a case of where to put it and when it's run.

In the case of this bug, it appears that certain initial conditions on components will cause the generated site to behave incorrectly - from what I can work out, as you said above, you need to make sure that all elements are statically generated as visible, but then this generates visual glitches in some cases.

If we can work out why the script isn't working when the element is initially hidden then this wouldn't be a problem - the element generates correct HTML, my code is running and setting the variable correctly, but the Vue code isn't changing the display style on render.

PS: If I open the dev console and manually disable the display: none property so the component is shown, the banner works perfectly and the "Got it!" button hides the banner and sets local storage - it's literally on the initial page load that the Vue side of the code isn't being executed.

manniL commented 4 years ago

When this component is included in a project, it runs fine in dev mode and universal mode, but when a full static site is generated the note never appears

Interestingly, I hit the same issue in dev as well (on the initial request). When not using the Vuetify snackbar but a div, it works fine on the initial request. Seems to be an issue with the Vuetify component then and not related to the actual generation šŸ˜‹


In my case, almost all of the site is static except for some numbers on preview cards and comment threads on a blog. It would be great to enhance the documentation so that it's clear where to add JavaScript that will fetch this content when the static site is deployed, or even if this is possible at all after full static generation - there is obviously some JavaScript stuff going on but it's a case of where to put it and when it's run.

During generation, the final "state" is saved inside a payload.js. On the client-side, instead of calling fetch and asyncData again, the saved state will be used. More is described in the post linked in my previous answer. To make sure fetch is only called on the client-side (for dynamic content), you can use fetchOnServer: false on the component-level to toggle the behavior. Alternatively, you can also wrap components in a <ClientOnly> tag to render them only on the client-side. Last but not least, using lifecycle hooks called only on the client-side is also an option to ensure data is always fetched "fresh": beforeMount, mounted.

PS: Stale-why-revalidate might be interesting too for you - https://github.com/Kong/swrv

davydnorris commented 4 years ago

When this component is included in a project, it runs fine in dev mode and universal mode, but when a full static site is generated the note never appears

Interestingly, I hit the same issue in dev as well (on the initial request). When not using the Vuetify snackbar but a div, it works fine on the initial request. Seems to be an issue with the Vuetify component then and not related to the actual generation šŸ˜‹

If that were the case then I would expect to see issues when running in other modes but I don't - it's specifically when you have an initial 'hidden' state and use full static generation - it's really weird.

I'll try out fetchOnServer for my dynamic data - thanks for that pointer. I'll also try that with the above component and see if it gives me any joy.

Ideally what I really want is for as much of the blog as possible to render statically and I don't need to refresh counts or comments every single time the user navigates to and from the page - having it fetched once per session would be fine, so I want to find a hook that's in just the right spot. created () works well in this sense but fetch would also be really good.

drewbaker commented 3 years ago

Pretty sure this is related to this issue: https://github.com/nuxt/nuxt.js/issues/8107

davydnorris commented 3 years ago

I have managed to work around the web UI part of my issue - in the end I went back to using a v-footer for my GDPR note instead of a snackbar, and I set up a local style class

.hidden {
  display: none;
}

I then used :class="showAccepted ? 'hidden' : ''" on the v-footer element and set the initial value of showAccepted to false.

When statically rendered using generate, the footer is no longer compiled out like it was when i used a v-if, and it had the hidden class applied to it. The page code then correctly displayed it as required