horprogs / vue-ssr-hmr

Boilerplate of VueJS application, which includes server-side rendering, hot reloading, VueX state management, CSS modules, code splitting, ESLint and customizable configs.
138 stars 30 forks source link

ServerPretech doesnt work #15

Closed jacquesmatthieu closed 4 years ago

jacquesmatthieu commented 4 years ago

Hi dude, I've used your boiterplate but I've got a probleme with serverPrefetch().

I have an API that returns data to me.

I launch this call via a dispatch in serverPrefetch() to fill my store but when I want to use the getters this one is undefined, you would know why?

<template>
    <div>
        <section class="tw-container">
            <v-breadcrumb-component :exam="currentExam" class="sm:tw-hidden md:tw-block lg:tw-block tw-hidden"
            />
            <v-title-level1-component>
                {{ mainTitle() }}
            </v-title-level1-component>
            <v-research-form-component class="sm:tw-mb-10 md:tw-mb-8 lg:tw-mb-8 tw-mb-8" />

            <v-empty-result-component
                v-if="!hasOneExamAcademyPublished"
                v-show="!pending"
                :exam="examSlug"
                :exam-code="currentExam.code"
            />
            <div class="sm:tw-block md:tw-flex">
                <v-exn-exam-text :exam-slug="examSlug"/>
            </div>
        </section>
        <v-mini-block-component :blocks-slug="blocksSlug" :exam-slug="examSlug" />
        <section class="tw-container">
            <v-title-level2-component>{{ $t('exn.resultsByExamAcademy') }}</v-title-level2-component>
            <v-list-component :exam-slug="examSlug" :exam-academies="examAcademies"/>
        </section>
    </div>
</template>

<script>
import { mapGetters } from 'vuex';
import VBreadcrumbComponent from '../components/breadcrumbComponent.vue';
import VResearchFormComponent from '../components/researchFormComponent.vue';
import VListComponent from '../components/listComponent.vue';
import VTitleLevel1Component from '../components/titleLevel1Component.vue';
import VTitleLevel2Component from '../components/titleLevel2Component.vue';
import VMiniBlockComponent from '../components/miniBlockComponent/miniBlockComponent.vue';
import VEmptyResultComponent from '../components/emptyResultComponent.vue';
import VExnExamText from './text/exnExamText.vue';

export default {
    name: 'exnExamPage',
    components: {
        VBreadcrumbComponent,
        VResearchFormComponent,
        VListComponent,
        VTitleLevel1Component,
        VTitleLevel2Component,
        VEmptyResultComponent,
        VMiniBlockComponent,
        VExnExamText
    },
    metaInfo() {
        return {
            title: this.title(),
            meta: [
                {
                    vmid: 'description',
                    name: 'description',
                    content: this.description()
                },
                {
                    property: 'og:title',
                    content: this.title()
                },
                {
                    property: 'og:description',
                    content: this.description()
                },
                {
                    property: 'og:locale',
                    content: 'fr_FR'
                },
                {
                    property: 'og:url',
                    content: this.canonical()
                },
                {
                    property: 'og:type',
                    content: 'article'
                },
                {
                    property: 'og:image',
                    content: 'https://storage.googleapis.com/prod-phoenix-bucket/exn/img_pages_exams.jpg'
                },
                {
                    property: 'og:image:width',
                    content: '590'
                },
                {
                    property: 'og:image:height',
                    content: '_'
                }
            ],
            link: [{ rel: 'canonical', href: this.canonical() }]
        };
    },
    props: {
        currentYear: {
            type: String,
            default: ''
        },
        examSlug: {
            type: String,
            required: true
        }
    },
    data: () => ({
        blocksSlug: [
            'coaching_revisions',
            'date_resultat_quand_1',
            'date_resultat_quand_2',
            'date_examen_epreuve_1',
            'date_examen_epreuve_2',
            'parcoursup_1',
            'parcoursup_2',
            'parcoursup_3',
            'que_faire_apres_bts_1',
            'que_faire_apres_bts_2',
            'que_faire_apres_bts_3',
            'que_faire_apres_recherche_emploi_1',
            'que_faire_apres_recherche_emploi_2'
        ],
        pending: true
    }),
    computed: {
        ...mapGetters({
            examAcademies: 'examAcademies',
            currentExam: 'currentExam'
        }),
        hasOneExamAcademyPublished: function hasOneExamAcademyPublished() {
            return this.examAcademies.some((element) => {
                return element.available;
            });
        }
    },
    serverPrefetch() {
        return this.getExamAcademies();
    },
    mounted() {
        if (!this.currentExam) {
            this.getExamAcademies();
        }

        this.$store.watch((state, getters) => getters.examAcademies,
            // eslint-disable-next-line no-unused-vars
            (newValue, oldValue) => {
                if (newValue.length > 0) {
                    this.pending = false;
                }
            });
    },
    methods: {
        getExamAcademies() {
            this.$store.dispatch('currentExam', { examSlug: this.examSlug });
            this.$store.dispatch('getExamAcademies', {
                examSlug: this.examSlug
            });
        },
        title() {
            if (this.currentExam) {
                return `${this.$t('exn.exnExamPage.title')}`
                    .replace('{ year }', this.currentYear)
                    .replace('{ exam }', this.currentExam.label);
            }

            return '';
        },
        description() {
            return `${this.$t('exn.exnExamPage.description')}`
                .replace('{ year }', this.currentYear)
                .replace(/{ exam }/g, this.currentExam.label);
        },
        mainTitle() {
            return `${this.$t('exn.exnExamPage.mainTitle')}`
                .replace('{ year }', this.currentYear)
                .replace('{ exam }', this.currentExam.label);
        },
        canonical() {
            // eslint-disable-next-line no-undef
            return `${BASE_URL}/resultat/${this.examSlug}.html`;
        }
    }
};
</script>
horprogs commented 4 years ago

Hey man! At first sight, serverPrefetch must return promise

jacquesmatthieu commented 4 years ago

So I must use an async / await ?

horprogs commented 4 years ago
getExamAcademies() {
    return Promise.all(
        [this.$store.dispatch('currentExam', { examSlug: this.examSlug }),
            this.$store.dispatch('getExamAcademies', {
                examSlug: this.examSlug
            })])
},

Please try something like this

horprogs commented 4 years ago
<!-- Item.vue -->
<template>
  <div v-if="item">{{ item.title }}</div>
  <div v-else>...</div>
</template>

<script>
export default {
  computed: {
    // display the item from store state.
    item () {
      return this.$store.state.items[this.$route.params.id]
    }
  },

  // Server-side only
  // This will be called by the server renderer automatically
  serverPrefetch () {
    // return the Promise from the action
    // so that the component waits before rendering
    return this.fetchItem()
  },

  // Client-side only
  mounted () {
    // If we didn't already do it on the server
    // we fetch the item (will first show the loading text)
    if (!this.item) {
      this.fetchItem()
    }
  },

  methods: {
    fetchItem () {
      // return the Promise from the action
      return this.$store.dispatch('fetchItem', this.$route.params.id)
    }
  }
}
</script>
horprogs commented 4 years ago

So, you should return this.$store.dispatch in your action, or if you have multiple dispatch functions return it in Promise all. I think it should work

jacquesmatthieu commented 4 years ago

It doesn't work ... I don't know why, Have you got vscode to share my IDE please ? :D

horprogs commented 4 years ago

I'll try to check it for few next hours. Maybe do you have any repo to make reproducing easier?

horprogs commented 4 years ago

Seems, this problem is unrelated to component. I tried to reproduce it and it works! Have a look:

<template>
  <div>
    {{ currentExam }}
    <br>
    {{ examAcademies }}
  </div>
</template>

<script>
import { mapGetters } from 'vuex';

export default {
  name: 'exnExamPage',
  components: {},
  metaInfo() {
    return {
      title: '',
      meta: [
        {
          vmid: 'description',
          name: 'description',
        },
        {
          property: 'og:title',
        },
        {
          property: 'og:description',
        },
        {
          property: 'og:locale',
          content: 'fr_FR',
        },
        {
          property: 'og:url',
        },
        {
          property: 'og:type',
          content: 'article',
        },
        {
          property: 'og:image',
          content:
            'https://storage.googleapis.com/prod-phoenix-bucket/exn/img_pages_exams.jpg',
        },
        {
          property: 'og:image:width',
          content: '590',
        },
        {
          property: 'og:image:height',
          content: '_',
        },
      ],
      link: [
        {
          rel: 'canonical',
        },
      ],
    };
  },
  props: {
    currentYear: {
      type: String,
      default: '',
    },
    examSlug: {
      type: String,
      required: true,
    },
  },
  data: () => ({
    blocksSlug: [
      'coaching_revisions',
      'date_resultat_quand_1',
      'date_resultat_quand_2',
      'date_examen_epreuve_1',
      'date_examen_epreuve_2',
      'parcoursup_1',
      'parcoursup_2',
      'parcoursup_3',
      'que_faire_apres_bts_1',
      'que_faire_apres_bts_2',
      'que_faire_apres_bts_3',
      'que_faire_apres_recherche_emploi_1',
      'que_faire_apres_recherche_emploi_2',
    ],
    pending: true,
    $t: () => {},
  }),
  computed: {
    ...mapGetters({
      examAcademies: 'examAcademies',
      currentExam: 'currentExam',
    }),
    hasOneExamAcademyPublished: function hasOneExamAcademyPublished() {
      return this.examAcademies.some(element => {
        return element.available;
      });
    },
  },
  serverPrefetch() {
    return this.getExamAcademies();
  },
  mounted() {
    if (!this.currentExam) {
      this.getExamAcademies();
    }

    this.$store.watch(
      (state, getters) => getters.examAcademies,
      // eslint-disable-next-line no-unused-vars
      (newValue, oldValue) => {
        if (newValue.length > 0) {
          this.pending = false;
        }
      },
    );
  },
  methods: {
    getExamAcademies() {
      this.$store.dispatch('currentExam', { examSlug: 'Current exam' });
      this.$store.dispatch('getExamAcademies', {
        examSlug: 'Exam Academies',
      });
    },
  },
};
</script>

And store module:

import axios from 'axios';

import {
  MAIN__ITEM_DELELE,
  MAIN__ITEM_ADD,
  MAIN__ITEM_ADD_ASYNC,
  MAIN__GET_DATA,
  MAIN__SET_DATA,
} from '../const/main';

export default {
  namespaced: false,
  state: {
    items: [
      {
        id: 1,
        title: 'Milk',
      },
      {
        id: 2,
        title: 'Strawberry',
      },
      {
        id: 3,
        title: 'Egg',
      },
    ],
    dataFromApi: [],
    currentExam: null,
  },
  mutations: {
    [MAIN__ITEM_DELELE](state, { id }) {
      state.items = state.items.filter(item => item.id !== id);
    },
    [MAIN__ITEM_ADD](state, { item }) {
      const items = [...state.items];
      items.push(item);

      state.items = items;
    },

    [MAIN__SET_DATA](state, { data }) {
      state.dataFromApi = data;
    },
    currentExam(state, { examSlug }) {
      state.currentExam = examSlug;
    },
    examAcademies(state, { examSlug }) {
      state.examAcademies = examSlug;
    },
  },
  getters: {
    currentExam(state) {
      return state.currentExam;
    },
    examAcademies(state) {
      return state.examAcademies;
    },
  },
  actions: {
    [MAIN__ITEM_ADD_ASYNC]({ commit }, { item }) {
      setTimeout(() => {
        commit(MAIN__ITEM_ADD, { item });
      }, 1000);
    },
    async [MAIN__GET_DATA]({ commit }) {
      try {
        const resp = await axios.get(
          'https://jsonplaceholder.typicode.com/posts',
        );

        commit(MAIN__SET_DATA, { data: resp.data });
      } catch (e) {
        console.error('Error fetching data');
      }
    },
    async currentExam({ commit }, { examSlug }) {
      commit('currentExam', { examSlug });
    },
    async getExamAcademies({ commit }, { examSlug }) {
      commit('examAcademies', { examSlug });
    },
  },
};

image

Probably the problem with your store?

jacquesmatthieu commented 4 years ago

I solved it the problem with a simple return ;) Thanks dude.

Other things, how can I display a pageError with status code 404, currently I've got a 200

horprogs commented 4 years ago

So, you could add error logic to entry-server.js You should caught errors there and render error page. Maybe I'll add this in future