I've been stuck on a hydration problem with Vue.js for over 2 days now and can't seem to find a solution.
Context:
I have a page where I want to render server side some articles so that I have them in my source code for better SEO. These articles are loaded from my Firestore and then sent to a sub component to be displayed.
This is my page component :
index.js (updated)
<template>
<div>
<AppHeader class="fixed-header"></AppHeader>
<section id="intro" class="px-md">
<div class="intro has-text-white p-none mt-lg py-lg">
<div class="is-hidden-touch pt-lg"></div>
<div class="columns is-mobile is-centered mb-none p-none">
<div class="intro-content column pt-lg is-10-mobile is-8-tablet is-6-desktop
max-content-width has-text-centered">
<h1 class="titre main-title is-size-4-mobile is-size-3-tablet is-size-2-desktop
has-text-weight-semibold">
Première plateforme de transmission de startups & TPE
</h1>
<p class="main-subtitle has-text-weight-medium mt-md">
Nous aidons les entrepreneurs souhaitant arrêter leur activité
à revendre leur travail, innovation ou savoir-faire.
</p>
<div class="ctas columns my-lg">
<div class="column has-text-right has-text-centered-mobile"
data-aos="zoom-in" data-aos-delay="100" data-aos-once="true" data-aos-anchor="intro">
<nuxt-link to="/valorisation">
<b-button id="cta-seller"
class="button zoom is-primary is-rounded has-text-weight-semibold">
Estimer gratuitement
</b-button>
</nuxt-link>
</div>
<div class="column has-text-left has-text-centered-mobile"
data-aos="zoom-in" data-aos-delay="200" data-aos-once="true" data-aos-anchor="intro">
<nuxt-link to="/annonces">
<b-button id="cta-buyer"
class="button zoom is-secondary is-rounded has-text-weight-semibold">
Découvrir nos annonces
</b-button>
</nuxt-link>
</div>
</div>
</div>
</div>
</div>
</section>
<div class="is-hidden-tablet has-text-centered" @click="scrollBelowIntro">
<b-icon icon="chevron-down" type="is-dark" size="is-large"></b-icon>
</div>
<section id="storybee" class="columns is-centered is-mobile my-lg">
<div class="column is-10 max-content-width" data-aos="fade-up" data-aos-once="true" data-aos-delay="400">
<h2 class="title is-size-3" @click="show = !show">Storybee vous permet</h2>
<div class="columns is-mobile is-centered my-lg">
<div class="column is-narrow m-none p-none">
<div class="columns my-md">
<div class="column is-narrow has-text-centered">
<b-icon icon="lightbulb-on-outline" type="is-tertiary" size="is-large"></b-icon>
</div>
<div class="column">
<h3 class="title is-size-5 has-text-weight-bold mb-sm">
De reprendre ou revendre une entreprise
</h3>
<p>
Via des <span class="has-text-weight-semibold">outils digitaux</span> simples, rapides & transparents
</p>
</div>
</div>
<div class="columns my-md">
<div class="column is-narrow has-text-centered">
<b-icon icon="check-all" type="is-tertiary" size="is-large"></b-icon>
</div>
<div class="column">
<h3 class="title is-size-5 has-text-weight-bold mb-sm">
D’être accompagné de A à Z
</h3>
<p>
Aucune prise de tête juridique ou financière,
<span class="has-text-weight-semibold">notre équipe est là pour vous !</span>
</p>
</div>
</div>
<div class="columns my-md">
<div class="column is-narrow has-text-centered">
<b-icon icon="arrow-right-box" type="is-tertiary" size="is-large"></b-icon>
</div>
<div class="column">
<h3 class="title is-size-5 has-text-weight-bold mb-sm">
D’accèder à des opportunités uniques
</h3>
<p>
Avec une base d’annonces <span class="has-text-weight-semibold">vérifiées</span>
& des repreneurs <span class="has-text-weight-semibold">qualifiés</span>
</p>
</div>
</div>
<div class="columns my-md">
<div class="column is-narrow has-text-centered">
<b-icon icon="flash" type="is-tertiary" size="is-large"></b-icon>
</div>
<div class="column">
<h3 class="title is-size-5 has-text-weight-bold mb-sm">
De bénéficier d’une méthode simplifiée
</h3>
<p>
Vous choisissez ce que vous voulez racheter,
pour une transmission <span class="has-text-weight-semibold">simple & efficace</span>
</p>
</div>
</div>
</div>
</div>
</div>
</section>
<section id="latest-sales" class="page-content-fullwidth rel has-background-light pb-none">
<div class="columns is-mobile is-centered">
<div class="column is-10 max-content-width">
<h2 class="title is-size-3 pt-lg m-none">Nos dernières annonces</h2>
<LatestSales></LatestSales>
</div>
</div>
<div class="columns is-mobile pb-lg">
<div class="column has-text-centered">
<nuxt-link to="/annonces">
<span data-aos="zoom-in" data-aos-delay="200" data-aos-once="true"
data-aos-anchor="#latest-sales">
<b-button class="zoom has-text-weight-semibold" icon-right="chevron-right" rounded
type="is-secondary">
Découvrir toutes les annonces
</b-button>
</span>
</nuxt-link>
</div>
</div>
</section>
<section class="columns is-centered mt-xl">
<div class="column is-10 max-content-width">
<h2 class="title is-size-3 ml-lg">Les atouts de la transmission</h2>
<div class="sell columns is-centered">
<div class="column is-hidden-tablet m-none p-none mt-lg">
<div class="columns is-mobile">
<div class="column is-6 is-offset-3 p-none">
<img class="illustration petal top-left" src="~assets/images/illustrations/transmit.jpg" loading="lazy" width="354" height="354" />
</div>
</div>
</div>
<div class="column">
<!-- <div class="bubble bubble-1">
<img src="~assets/images/storybee/petal-storybee-coral.svg"/>
</div>
<div class="bubble bubble-2">
<img src="~assets/images/storybee/petal-storybee-coral.svg"/>
</div>
<div class="bubble bubble-3">
<img src="~assets/images/storybee/petal-storybee-coral.svg"/>
</div>
<div class="bubble bubble-4">
<img src="~assets/images/storybee/petal-storybee-coral.svg"/>
</div> -->
<div id="transmit" class="content columns is-centered mt-lg">
<div class="column is-5 pr-none pt-xl mt-lg">
<div class="title-sell title-left p-sm pl-md">
<h2 class="has-text-white is-pulled-left ml-md">Je transmets...</h2>
</div>
<div style="clear: both"></div>
<div class="has-text-right m-lg">
<div data-aos="fade-in-left" data-aos-delay="300" data-aos-once="true" class="mb-md">
<h3 class="is-size-5 has-text-dark is-uppercase">
<b-icon type="is-primary" icon="chevron-right-circle"></b-icon>
Ce pour quoi j’ai travaillé dur
</h3>
<p>J’ai créé, développé un savoir-faire ou innové et transmets
l’ensemble de mon travail (marque, produits, services, site internet etc.)</p>
</div>
<div data-aos="fade-in-left" data-aos-delay="300" data-aos-once="true" class="mb-md">
<h3 class="is-size-5 has-text-dark is-uppercase">
<b-icon type="is-primary" icon="chevron-right-circle"></b-icon>
Quel que soit mon statut
</h3>
<p>Que j’aie une micro-entreprise ou TPE, qu’elle ait eu le temps d’avoir du succès ou non !</p>
</div>
<div data-aos="fade-in-left" data-aos-delay="300" data-aos-once="true" class="mb-md">
<h3 class="is-size-5 has-text-dark is-uppercase">
<b-icon type="is-primary" icon="chevron-right-circle"></b-icon>
Pour valoriser mon travail
</h3>
<p>Je souhaite récupérer mon investissement, faire en sorte que mon
travail ne se perde pas & valoriser mon expérience</p>
</div>
<nuxt-link to="pages/transmettre">
<span data-aos="zoom-in" data-aos-delay="300" data-aos-once="true">
<b-button type="is-primary" rounded class="zoom has-text-weight-semibold m-sm"
icon-right="chevron-right">
En savoir plus
</b-button>
</span>
</nuxt-link>
</div>
</div>
<div class="column is-hidden-mobile is-5 pr-none pt-xl m-none mt-lg">
<div class="title-sell title-right p-sm pl-md"></div>
<img class="illustration petal top-left" src="~assets/images/illustrations/transmit.jpg" loading="lazy" />
</div>
</div>
</div>
</div>
</div>
</section>
<section class="columns is-centered mb-xl">
<div class="column is-10 max-content-width">
<div class="buy columns is-centered">
<div class="column is-hidden-tablet m-none p-none mt-xl">
<div class="columns is-mobile">
<div class="column is-6 is-offset-3 p-none">
<img class="illustration petal bottom-right" src="~assets/images/illustrations/continue.jpg" loading="lazy" width="354" height="354" />
</div>
</div>
</div>
<div class="column">
<!-- <div class="bubble bubble-1">
<img src="~assets/images/storybee/petal-storybee-turquoise.svg"/>
</div>
<div class="bubble bubble-2">
<img src="~assets/images/storybee/petal-storybee-turquoise.svg"/>
</div>
<div class="bubble bubble-3">
<img src="~assets/images/storybee/petal-storybee-turquoise.svg"/>
</div>
<div class="bubble bubble-4">
<img src="~assets/images/storybee/petal-storybee-turquoise.svg"/>
</div> -->
<div id="continue" class="content columns is-centered mt-lg">
<div class="column is-hidden-mobile is-5 pr-none pt-xl m-none mt-lg">
<div class="title-buy title-left p-sm pl-md"></div>
<img class="illustration petal bottom-right" src="~assets/images/illustrations/continue.jpg" loading="lazy" />
</div>
<div class="column is-5 pl-none pt-xl mt-lg">
<div class="title-buy title-right p-sm pl-md">
<h2 class="has-text-white is-pulled-right mr-md">Je reprends</h2>
</div>
<div class="m-lg">
<div data-aos="fade-in-left" data-aos-delay="300" data-aos-once="true" class="mb-md">
<h3 class="is-size-5 has-text-dark is-uppercase">
<b-icon type="is-success" icon="chevron-right-circle"></b-icon>
Pour me lancer ou me diversifier
</h3>
<p>En rachetant, je capitalise sur l’existant, diminue mon risque & maximise
mes chances de réussite</p>
</div>
<div data-aos="fade-in-left" data-aos-delay="300" data-aos-once="true" class="mb-md">
<h3 class="is-size-5 has-text-dark is-uppercase">
<b-icon type="is-success" icon="chevron-right-circle"></b-icon>
Pour gagner du temps
</h3>
<p>Je gagne des compétences complémentaires, de l’expérience, une belle
histoire et le plus précieux pour réussir... du temps ! </p>
</div>
<div data-aos="fade-in-left" data-aos-delay="300" data-aos-once="true" class="mb-md">
<h3 class="is-size-5 has-text-dark is-uppercase">
<b-icon type="is-success" icon="chevron-right-circle"></b-icon>
Pour faire des économies
</h3>
<p>Créer ou innover peut coûter très (très) cher. Vous gérez votre budget selon le stade de
développement de l’entreprise, la reprise n’a jamais été aussi accessible !</p>
</div>
<nuxt-link to="/pages/reprendre">
<span data-aos="zoom-in" data-aos-delay="300" data-aos-once="true">
<b-button type="is-secondary" rounded class="zoom has-text-weight-semibold m-sm"
icon-right="chevron-right">
En savoir plus
</b-button>
</span>
</nuxt-link>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<section id="partners" class="partners columns is-centered has-background-light pt-xl pb-lg">
<div class="column is-10 max-content-width" data-aos="fade-up" data-aos-delay="200" data-aos-once="true">
<div class="columns is-mobile">
<div class="title-container column is-narrow has-background-white px-lg py-md">
<h2 class="titre is-size-3 has-text-dark has-text-weight-semibold has-background-white">
Ils nous soutiennent
</h2>
</div>
</div>
<div class="level">
<div class="level-item my-lg">
<a href="https://lafrenchtech.com/" target="_blank">
<img class="partner-logo" src="~assets/images/logos/logo-french-tech.png" width="215" height="128" loading="lazy" />
</a>
</div>
<div class="level-item my-lg">
<a href="https://www.techforgoodfr.org/" target="_blank">
<img class="partner-logo" src="~assets/images/logos/logo-techforgood.png" width="200" height="155" loading="lazy"/>
</a>
</div>
<div class="level-item my-lg">
<a href="https://www.bpifrance.fr/" target="_blank">
<img class="partner-logo" src="~assets/images/logos/logo-bpifrance.png" width="300" height="80" loading="lazy"/>
</a>
</div>
<div class="level-item my-lg">
<a href="http://www.poleactionmedia.com/" target="_blank">
<img class="partner-logo" src="~assets/images/logos/logo-pole-action-media.png" width="250" height="134" loading="lazy"/>
</a>
</div>
<div class="level-item my-lg">
<a href="https://impactfrance.eco/" target="_blank">
<img class="partner-logo" src="~assets/images/logos/logo-impact-france.png" width="340" height="148" loading="lazy"/>
</a>
</div>
</div>
</div>
</section>
<section id="products" class="products columns is-centered has-background-light pb-lg">
<div class="column max-content-width px-lg">
<div class="columns is-mobile mb-xl">
<div class="title-container column is-narrow has-background-white px-lg py-md">
<h2 class="titre title is-size-3 has-text-dark has-text-weight-semibold has-background-white">
Nos outils
</h2>
</div>
</div>
<div class="columns is-centered is-mobile" data-aos="fade-up" data-aos-once="true">
<div class="column is-10 toolcard py-none">
<ToolCard button-color="is-primary"
button-link="/valorisation"
button-text="Estimer"
image="outils"
title="Estimez la valeur de votre petite ou micro entreprise"
class="my-lg">
</ToolCard>
</div>
</div>
<div class="columns is-centered is-mobile" data-aos="fade-up" data-aos-once="true">
<div class="column is-10 toolcard py-none">
<ToolCard button-color="is-secondary"
button-link="/buyer-search"
button-text="Activer"
image="annonces"
title="Activ’search - Ma veille active"
class="my-lg">
</ToolCard>
<!-- <ToolCard button-color="is-secondary"
button-link="/activ-search"
button-text="Activer ma veille"
image="annonces"
title="Activ’search - Ma veille active"
class="my-lg">
</ToolCard> -->
</div>
</div>
<div class="columns is-centered is-mobile" data-aos="fade-up" data-aos-once="true">
<div class="column is-10 toolcard py-none">
<ToolCard button-color="is-primary"
button-link="/articles/guide-transmission"
button-text="Lire les articles"
image="cedant"
title="Le guide de la transmission"
class="my-lg">
</ToolCard>
</div>
</div>
<div class="columns is-centered is-mobile" data-aos="fade-up" data-aos-once="true">
<div class="column is-10 toolcard py-none">
<ToolCard button-color="is-secondary"
button-link="/articles/guide-transmission"
button-text="Lire le guide"
image="repreneur"
title="Le guide de la reprise"
class="my-lg">
</ToolCard>
</div>
</div>
</div>
</section>
<section id="articles" class="articles mt-lg"
data-aos="fade-up" data-aos-delay="400" data-aos-anchor="bottom-center" data-aos-once="true">
<div class="background"></div>
<div class="columns is-centered is-mobile mt-none">
<div class="column is-10-desktop is-12-touch max-content-width pt-none">
<h2 class="titre is-size-3 has-text-weight-semibold has-text-dark my-md"
data-aos="fade-up" data-aos-delay="100" data-aos-once="true">
Nos derniers blogs
</h2>
<div class="level">
<div v-for="(article, articleIndex) in articles" :key="articleIndex" class="level-item mx-sm">
<ArticleCard @error="setArticleError" :article="article" class="mt-lg">
</ArticleCard>
</div>
</div>
</div>
</div>
</section>
<div class="fixed-contact zoom"
data-aos="zoom-in" data-aos-once="true" data-aos-delay="500" data-aos-anchor="#intro">
<!-- <img src="~assets/images/icons/question-mark.svg" /> -->
<b-button type="is-primary" icon-left="phone" rounded class="zoom"
@click="isContactModalActive = true">
Nous contacter
</b-button>
</div>
<b-modal v-model="isContactModalActive"
:can-cancel="['escape', 'outside']"
:full-screen="$device.isMobile">
<div class="sb-modal has-rounded-corners p-lg">
<div class="is-pulled-right is-clickable" @click="isContactModalActive = false">
<b-icon icon="close"></b-icon>
</div>
<div>
<h4 class="is-size-5 has-text-weight-semibold mb-md">Besoin de renseignements ?</h4>
<div class="sb-modal-content">
<p>
Vous pouvez nous contacter au
<a href="tel:0698497479" class="has-text-weight-semibold">06 98 49 74 79</a>,
ou entrer votre nom et numéro ci-dessous pour qu'on vous rappelle :
</p>
<CallForm :context="{
origin: 'Home Page: boutton contact'
}">
</CallForm>
</div>
</div>
</div>
</b-modal>
<Newsletter></Newsletter>
<AppFooter @show-contact="isContactModalActive = true"></AppFooter>
</div>
</template>
<script>
import { mapState, mapGetters } from 'vuex'
import _map from 'lodash/map'
// import { KinesisContainer, KinesisElement} from 'vue-kinesis'
import AppHeader from '@/components/menus/AppHeader'
import AppFooter from '@/components/menus/AppFooter'
import ArticleCard from '@/components/articles/ArticleCard'
import LatestSales from '@/components/sales/LatestSales'
import Newsletter from '@/components/Newsletter'
import ToolCard from '@/components/ToolCard'
import CallForm from '@/components/forms/CallForm'
import {articleConverter} from "data-model";
export default {
layout: (ctx) => ctx.isMobile ? 'bare-mobile' : 'bare',
components: {
AppHeader,
AppFooter,
Newsletter,
ArticleCard,
LatestSales,
ToolCard,
// KinesisContainer,
// KinesisElement
CallForm,
},
async fetch() {
console.log("FETCHHHHHH");
await this.loadArticles();
},
data() {
return {
isLoading: true,
focusedArticleIndex: 0,
isContactModalActive: false,
articleError: false,
articles: null,
formData: {
email: null
}
}
},
computed: {
...mapState({
loggedInUser: (state) => state.loggedInUser.user,
authUser: (state) => state.auth.user
}),
...mapGetters({
isAuthenticated: 'auth/isAuthenticated',
hasLoggedInUser: 'loggedInUser/hasLoggedInUser'
}),
},
async mounted() {
if (this.$route.query.section) {
this.$scrollTo(`#${this.$route.query.section}`, 300, { offset: -100 })
}
},
methods: {
scrollBelowIntro() {
this.$scrollTo('#storybee', 300, { offset: -100 });
},
setArticleError() {
this.articleError = true;
},
subscribe() {
this.$fire.functions.httpsCallable('mailchimp-subscribeToNewsletter')({
email: this.formData.email
})
.then(() => {
// console.log('SUCCESS', result)
this.$buefy.toast.open({
duration: 5000,
message: 'On vous a bien inscrit ! On vous tient au courant très vite Merci !',
position: 'is-bottom',
type: 'is-success'
})
})
.catch((err) => {
console.error('ERROR', err.code, err.message, err.details)
const code = err.code
let errorMessage = 'Une erreur est survenue. Ré-essayez plus tard ou contactez-nous à <strong>hello@storybee.fr</strong>'
if (code === 'invalid-argument')
errorMessage = err.message
this.$buefy.toast.open({
duration: 5000,
message: errorMessage,
position: 'is-bottom',
type: 'is-danger'
})
})
},
async loadArticles() {
console.log("inside loadArticles");
const articles = [];
try {
this.isLoading = true;
const querySnapshot = await this.$fire.firestore.collection("blogPosts")
// .where("publishedDate", "<", new Date())
.limit(3)
.get();
// console.log("RESULT", querySnapshot);
querySnapshot.forEach((articleDoc) => {
const article = articleConverter.fromFirestoreData(articleDoc.data());
articles.push(article);
});
console.log("articles IDs", _map(articles, (article) => article.articleId));
this.articles = articles;
this.isLoading = false;
}
catch(err) {
console.error("ERROR", err);
this.articleError = true;
}
}
},
}
</script>
<style lang="sass" scoped>
// @import '~assets/styles/intro-rosaces.sass'
@import '~assets/styles/modal.sass'
@import '~assets/styles/landing.sass'
.fixed-contact
position: fixed
bottom: 3vh
right: 2vw
.button
box-shadow: 0px 2px 4px $gs3
</style>
This is were I call my loadArticles function:
async fetch() {
console.log("FETCHHHHHH");
await this.loadArticles();
},
And this is the loadArticlesFunction:
async loadArticles() {
console.log("hehehe");
try {
this.isLoading = true;
this.$fire.firestore.collection("blogPosts")
.where("publishedDate", "<", new Date())
.get()
.then((querySnapshot) => {
const articles = [];
querySnapshot.forEach((articleDoc) => {
const article = articleConverter.fromFirestoreData(articleDoc.data());
articles.push(article);
});
console.log("this are the articles", articles);
this.articles = articles;
this.isLoading = false;
})
.catch((error) => {
this.articleError = true;
});
} catch(err) {
this.articleError = true;
}
I have encapsulated the v-for with a client-only as said in documentation.
To debug, I've set various breakpoints to be able to read the elm object from the hydrate function and try and found the error message in this html object but no luck. Ive also tried to user asyncData instead of fetch but can't get access to this. My latest test was to use a promise to force Nuxt to wait for the fetch to finish but no luck with that either.
I think the problem is that mounted is executed before the end of my fetch, thus causing this error.
EDIT
If I add a console.log in the catch, I get this:
ERROR Error [FirebaseError]: Function Query.where() called with invalid data. Unsupported field value: a custom Date object
So if I comment out this line:
.where("publishedDate", "<", new Date())
It all works fine.
But now I have no idea why new Date doesn't work here.... and what to replace it with...