9

I'm trying to get server side rendering to work in VueJS.

I've been following the official docs, and I'm attempting to get this example to work using axios. The endpoint is correct and the data does show up in the mutation.

https://ssr.vuejs.org/guide/data.html

I also found this page and tried most of these examples, which includes using getters, vuex mapState, mapGetter, etc:

vue.js 2 how to watch store values from vuex

Here is store/index.js:

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

import { fetchPage } from './api'

export function createStore () {
    return new Vuex.Store({
        strict: true,

        state: () => ({
            res: {}
        }),

        actions: {
            actXY ({ commit }, page) {
                fetchPage('homepage').then((item) => {
                    commit('mutXY', { page, item });
                }).catch(error => {
                    throw new Error(`Store ${error}`);

                })
            }
        },

        mutations: {
            mutXY (state, { page, item }) {
                Vue.set(state.res, page, item);
                console.log(state.res.homepage.data)

            }
        },

        getters: {
            getXY (state) {
                return state.res
            }
        }
    })
}

the console log is displaying the correct information, so that means the axios endpoint is working and updating the state object. Originally, I was attempting to call the mutation, but I've since tried calling the getter. Unfortunately, the new data is not being updated in the getter, so of course, nothing new shows up in the component.

This is HomePage.vue:

<template>
  <div v-if="htmlData">{{ JSON.stringify(htmlData) }}</div>
  <div v-else>loading?</div>
</template>

<script>
export default ({
    name: 'homepage',

    computed: {
        htmlData () {
            console.log("computed = " + JSON.stringify(this.$store.state.res));
            console.log(this.$store.getters.getXY);
            return this.$store.getters
            // return this.getQuery()
        }
    },

    serverPrefetch () {
        return this.getData()
    },

    mounted () {
        console.log("in mounted")
        if (!this.item) {
            this.getData()
        }
    },

    methods: {
        getData () {
            return this.$store.dispatch('actXY', 'homepage')
        }
    }
})

</script>

As written, htmlData will show:

{"getXY":{}}

Yes, I did attempt many other variations on how to return the store item in the getter, but nothing is working.

Aside from the stuff in the above links, I've also looked around for variations on config files, I attempted to add async to store/actions, getData(), etc.

I also attempted to make the axios call directly in the component, which has had no success.

Since this is a project that was already more-or-less completed in VueJS that I'm converting to SSR, I removed everything from package.json and reinstalled every package, in the hopes that maybe one of the old vue packages was causing a conflict.

I also attempted the store code splitting from the official docs, and tried variations on how the routes are written. Nothing at all works.

I think I should add what the print statements show when I run my current code:

computed = undefined
computed mm  = undefined
getter = {}
computed get  = {"getXY":{}}
{
  title: 'Home'...}

The computed property in the component runs before the mutation is set. This causes the getter to be called before the mutation is updated. Likewise, if I'm attempting to call changes to res directly from the component, there is nothing in mutations when the store is being called and rendered.

This is a repo of the code I'm attempting to run: https://github.com/dt1/vue-ssr-attempt

(I figured out the answer. I've updated the repo with what is working for me, and explanation of my solution is below)

dizzystar
  • 1,055
  • 12
  • 22

4 Answers4

1

First storeData() => storeData(state)

then return this.$store.state.items.homepage.data => return this.$store.state.items.homepage && return this.$store.state.items.homepage.data (or initialize the state items with an empty homepage)

Renaud
  • 1,290
  • 8
  • 8
  • I don't understand this answer. Why is there a &&? – dizzystar May 17 '20 at 05:59
  • Because this.$store.state.items.homepage is initially undefined, until the data are comited. You don't want to access .data property of undefined ;) You should have an error message in the console regarding that. – Renaud May 17 '20 at 08:36
  • shouldn't it be `this.getData()` instead of `this.fetchItem()` in the mounted hook? – Renaud May 17 '20 at 08:38
  • I left the mount in there by accident. It doesn't matter with SSR since mounted never runs. I removed it, updated, etc, but nothing happens. – dizzystar May 17 '20 at 09:34
  • There is no error popping up. That's the issue, "computed" doesn't fire at all. If I add a watch property, computed will at least log out the "computed here", but there is no rendering or errors. It's very confusing. – dizzystar May 17 '20 at 09:38
  • Did you try `return this.$store.state.items.homepage && return this.$store.state.items.homepage.data` for the item computed? We'll try to move forward from there. – Renaud May 17 '20 at 20:37
  • Nothing happens. The $this.store is just the default mapping, like this `{"items":{},"route":{"name":"homepage"....}`, even inside `computed` after adding a watcher. It's also strange because `getter` can't seem to see any state changes either. – dizzystar May 17 '20 at 23:51
  • If you could set-up a minimal reproduction that would help greatly I think (https://stackoverflow.com/help/minimal-reproducible-example) – Renaud May 18 '20 at 00:10
1

There is no SSR in Vue.js out of the box. In your example you can get data on client.

Try this example for Store:

export default new Vuex.Store({
  strict: true,
  state: {
    res: null
  },
  getters: {
    getXY(state) {
      return state.res;
    }
  },
  mutations: {
    mutXY(state, payload) {
      state.res = payload;
    }
  },
  actions: {
    actXY: function(store) {
      fetchPage("homepage")
        .then(res => {
          store.commit("mutXY", res);
        })
        .catch(error => {
          throw new Error(`Store ${error}`);
        });
    }
  }
});

And you can get data in the component as such:

  computed: {
    htmlData() {
      return this.$store.getters.getXY;
    }
  },
  created() {
    this.getData();
  },
  methods: {
    getData() {
      return this.$store.dispatch("actXY", "homepage");
    }
  }
tony19
  • 125,647
  • 18
  • 229
  • 307
Dvdgld
  • 1,984
  • 2
  • 15
  • 41
  • The only way I'm able to get getters to work is if I use a watcher in my component. Asdie from this, the store data is invisible in getters. It seems that, once the data is set in the mutations property, it is locally-scoped and can't be seen anywhere else. – dizzystar May 19 '20 at 06:04
  • Maybe it's due to Vue.set() in your mutation. Please try example of mutation that I've added above. – Dvdgld May 19 '20 at 06:44
  • Tried that, but still the same issue – dizzystar May 19 '20 at 07:03
  • Edited my answer. – Dvdgld May 19 '20 at 16:39
  • I'm aware that SSR isn't out of the box. I've been configuring this for an entire week now, and `{"getXY":{}}` does show up in view source. If I log `state` in getters or the compenent, it only shows the original `res` object, with now new information, and this is the issue. Whatever is in mutations cannot be seen outside of mutations. – dizzystar May 20 '20 at 02:57
1

My question is why is the this.getData() called in mounted?

If I understand the process correctly, when you call server side rendering you are trying to render the HTML before the vue component is mounted. Would it work if you called this.getData() in created instead of mounted?

Other things I would try:

  1. hardcode htmlData value in component to check if it would render
  2. if that works, hardcode htmlData value in store and check if the component can fetch that store data and render properly too
  3. after that works too, fetch htmlData from server via dispatch from the component, see if it works.
  4. if (3) does not work, try pushing the htmlData to a child component that is created only when you already have htmlData ready, send htmlData to the child component via props
Stefani
  • 142
  • 1
  • 2
  • 10
  • The mounted is just copy/paste from the docs. I had it removed before and it didn't make a difference (as expected). `serverPrefetch` is apparently the correct way to instantiate the store. I did try `created` and it doesn't work. I'm going to try out your other ideas and see what happens. – dizzystar May 22 '20 at 14:58
  • Okay, when I hard-code a return value of the map in the `fetchPage` function, the data is returned successfully. Seems the issue is when I'm using axios, the data does return, but then it is squashed right away, maybe? – dizzystar May 22 '20 at 15:28
  • Tried plan 4 with the props. The prop can fire but nothing will render. The data doesn't even get into mutations when doing that, so it's just an empty object. – dizzystar May 22 '20 at 16:36
  • so a hard-code fetchPage function works, but when you have to wait for axios it does not return anything. I suspect it is because the component you are trying to render has finished rendering before axios returns the value. So at (4) I tried to recreate the hard-code fetchPage by having data ready before creating the component. However I didn't expect the props to not work. If you tried
    {{ htmlData }}
    it becomes just empty object?
    – Stefani May 23 '20 at 06:03
  • That is correct, getter and mutation comes back as an empty object. The data from axios does return, it prints out in mutation but does not print out in getters. When the component calls the mutation directly, there is no data returned either. – dizzystar May 23 '20 at 14:59
  • Actually, forgot to mention, that actions, mutations, and getters did fire twice when I did v4. – dizzystar May 23 '20 at 20:33
  • it fires twice, so the first time it fire it does get the htmlData from server, but then it fires again and the data is gone? – Stefani May 24 '20 at 11:32
  • Where I am right now, the component would fire twist while using props. The actions and mutations fire once each, but then there would be no data in mutations. – dizzystar May 24 '20 at 20:57
  • hi, it's been awhile but I found something that might be useful https://ssr.vuejs.org/#about-this-guide about ssr in vue. seems you have to install an extra package and add an extra attribute to the html DOM for it to "hydrate" properly https://ssr.vuejs.org/guide/hydration.html – Stefani Jul 23 '20 at 03:58
1

Okay, I finally figured this out.

The main issues I ran into were the out-of-order calls. When I fixed the ordering, I was running into Converting circular structure to JSON errors. Next was trying to understand how the callbacks function between VueJS, Vuex stores, and axios.

The final solution I came up with is:

store/index.js

import Vue from 'vue'
import Vuex from 'vuex'
import * as util from 'util'

Vue.use(Vuex)

import { fetchPage } from './api'

export function createStore () {
    return new Vuex.Store({
        strict: true,

        state: () => ({
            res: {}
        }),

        actions: {
            actXY ({ commit }, page) {
                return fetchPage(page).then(item => {
                    commit('mutXY', { page , item } );
                })
            }
        },

        mutations: {
            mutXY (state, {page, item }) {
                Vue.set(state.res, page, item);
            }
        }
    })
}

Then when making the axios call, I had to be sure to return only the response.data instead of the entire response. Before this, I getting a unhandled promise error.

api.js

import Vue from 'vue';
import axios from 'axios';
import VueAxios from 'vue-axios'

Vue.use(VueAxios, axios);

Vue.axios.defaults.baseURL = '<url goes here>';

export function fetchPage(page){
    return Vue.axios.get(page).then((resp => resp.data))
}

Last but not least, this is Homepage.vue.

In order to fire an update, I had to make getData() asynchronous:

<template>
<div v-if="htmlData">
  <div>{{ htmlData.title }}</div>
  <div>{{ htmlData.description }}</div>
  <div>{{ htmlData.html }}</div>
</div>
</template>

<script>
export default ({
    name: 'homepage',
    computed: {
        htmlData () {
            return this.$store.state.res.homepage
        }
    },

    serverPrefetch () {
        return this.getData()
    },
    mounted () {
        if (!this.item) {
            this.getData()
        }
    },
    methods: {
        async getData () {
            await this.$store.dispatch('actXY', 'homepage')
        }
    }
})
</script>

Hopefully this helps anyone who's running into all of these issues.

dizzystar
  • 1,055
  • 12
  • 22