0

I have a component that renders just fine in SSR, but then, flickers when Vue 3 does the hydration (I think).

<template>
    <ul class="grid grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
      <li v-for="recipe in recipes" :key="recipe.id" class="col-span-1 flex flex-col text-center bg-white rounded-lg shadow divide-y divide-gray-200">
        <div class="flex-1 flex flex-col">
          <img class="w-full flex-shrink-0 mx-auto bg-black rounded-t-lg" :src="recipe.imageUrl" alt="" />
          <h3 class="text-gray-900 text-sm font-medium p-4">
            
              {{ recipe.title }}
          </h3>
        </div>
      </li>
    </ul>
    <Pagination v-model="page" :records="totalRecords" :per-page="12" :options="paginationOptions" @paginate="fetchRecipes" />
  </template>
  
  <script>
  
  import Pagination from 'v-pagination-3'
  import axios from 'axios'

  export default {
  
    components: {
      Pagination,
    },
    inject: ['paginationOptions'],
    data() {
      return {
        section: 'home',
        recipes: [],
        page: 3,
        totalRecords: 0,
      }
    },
    created() {
    },
    mounted() {
      this.fetchRecipes(this.page)
    },
    methods: {
      async fetchRecipes(page) {
        try {
          const url = 'http://local-api.local/api/fakeApi'
          const response = await axios.get(url, { params: { page } }).then((response) => {
            this.recipes = response.data.data.map(recipe => ({
              title: recipe.title,
              imageUrl: `http://test-server.local${recipe.thumbnailUrl}`,
            }))
            this.totalRecords = response.data.total
          })
        }
        catch (err) {
          if (err.response) {
            // client received an error response (5xx, 4xx)
            console.log('Server Error:', err)
          }
          else if (err.request) {
            // client never received a response, or request never left
            console.log('Network Error:', err)
          }
          else {
            console.log('Client Error:', err)
          }
        }
      },
    },
    serverPrefetch() {
      return this.fetchRecipes(this.page)
    },
  }
  </script>

What am I doing wrong? I must have tried 50 ways of setting this.recipes (in all possible lifecycle hooks too) and all of them still cause a flicker on hydration.

I'm using Vite (with vite-ssr plugin if it matters) / Vue 3 SSR. Note that the component after the flicker seems identical to the version generated by SSR which displays on page load (and is in the source).

kissu
  • 40,416
  • 14
  • 65
  • 133
Jeff B.
  • 1,117
  • 3
  • 16
  • 40
  • There are some suppositions (`I think`, `seems`) that need to be fixed either by a deeper debugging on your side or with a hosted project for people to see what actually happens. – kissu Jun 04 '21 at 06:18
  • is your fetching working try console logging that. If it is a valid response please let me know – codingwith3dv Jun 04 '21 at 06:20
  • Fetching works, if I remove the server prefetch part, then it only load after Vue has started. Opposite is the same, if I remove the fetching in mounted hook, then it shows on page load from the Ssr but disappears after Vue is started. That's what make me think fetching works both in Ssr and after. But still there is this flickering when Vue starts. Project is too big to be able to provide an example, but I'll try to think of a way. – Jeff B. Jun 04 '21 at 14:46
  • Also the fetching result is identical in console, another reason why I'm surprised it flickers. – Jeff B. Jun 04 '21 at 14:48

2 Answers2

2

I found the solution.

I had to put all my data in a Vuex Store (in component) and then in main.ts, use it as initialState, and when client is loaded, fill the store back with the initialState values.

Create store in main.ts:

import { createStore } from 'vuex'
const store = createStore({
  state() {
    return {}
  },
})

In my component, fill the store:

this.$store.state.pageContent = {
  recipes: response.data.data,
  totalRecords: response.data.total,
  totalPages: response.data.last_page,
  page,
}

And then in main.ts, but in the export default viteSSR():

if (import.meta.env.SSR) {
    initialState.initial = store.state
}
else {
    for (const item of Object.entries(initialState.initial)) {
        store.state[item[0]] = item[1]
    }

    //check if the store is identical as teh one generated by SSR
    console.log(store.state)
}

Please note that I used this Vite SSR / Vue 3 boilerplate project if you need to see more of the structure: https://github.com/frandiox/vitesse-ssr-template

kissu
  • 40,416
  • 14
  • 65
  • 133
Jeff B.
  • 1,117
  • 3
  • 16
  • 40
0

In the example, your mapping function with the api results does not include the id.

            this.recipes = response.data.data.map(recipe => ({
              title: recipe.title,
              imageUrl: `http://test-server.local${recipe.thumbnailUrl}`,
            }))

that means the :key=recipe.id is bound to undefined and vue cannot track changes per record. Change the mapping function to include the id

            this.recipes = response.data.data.map(recipe => ({
              id: recipe.id,
              title: recipe.title,
              imageUrl: `http://test-server.local${recipe.thumbnailUrl}`,
            }))

or just include the entire object if we feel like it

            this.recipes = response.data.data.map(recipe => ({
              ...recipe,
              imageUrl: `http://test-server.local${recipe.thumbnailUrl}`,
            }))

of course that assumes that there's an id property on recipes to begin with.

John Snow
  • 1,898
  • 2
  • 27
  • 48