Search code examples
vue.jsserver-side-renderingvuejs3hydrationvite

Prevent component flickering on Vite / Vue 3 SSR component hydration


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).


Solution

  • 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