'Vue dynamically adding routes from wordpress menus api, possible reaactivity problem

I have a vue application on the frontend and a wordpress api on the backend. I am hitting the menus api and dynamically adding routes to the frontend at run time.

This works great. Until I reset the page on one of the dynamic routes. The component does not load and mounted() is never called. At this point, I can click the router link in the nav bar and the page component renders as expected.

For example. In the wordpress admin, I create a page called hello-world and add it to the primary menu. Vue will hit the api and create a route with the same name. I then load up the page and it loads fine. I click the hello world link in the nav bar, and it renders beautifully.

Now, I'm sitting at http://website.com/hello-world, and I reset the page. The app mounts and the nav bar renders. However, the page component does not render. If I click the link in the nav bar again, then it renders fine.

I am thinking this is a reactivity problem, but I can't find it. Any suggestions?

Edit. Been pondering this. The router component is loaded, and fetches the menu items asynchronously. Now, Im already sitting on one of the dynamic routes, /hello-world. The app is now loaded and there doesn't exist yet a hello-world route, since the api request is still pending. Since there is no matching route, the vue application doesn't know which component to mount... Perhaps there is a way to make the router component itself reactive?

relevant router code...

store.dispatch("getPrimaryMenu").then(() => {
    store.state.menu.items.forEach((item) => {
        if (item.object === "post") {
            router.addRoute({
                path: `/${item.slug}`,
                name: item.slug,
                component: () => import("@/views/Post.vue"),
            });
        }
        if (item.object === "page") {
            router.addRoute({
                path: `/${item.slug}`,
                name: item.slug,
                component: () => import("@/views/Page.vue"),
            });
        }
    });
});

and my store...

export default createStore({
    state: {
        menu: {
            items: [],
        },
        page: {
            title: {},
            content: {},
        },
        post: {
            title: {},
            content: {},
        },
    },
    mutations: {
        SET_MENU(state, data) {
            state.menu = data
        },
        SET_PAGE(state, data) {
            state.page = data
        },
        SET_POST(state, data) {
            state.post = data
        },
    },
    actions: {
        getPrimaryMenu({ commit, state }) {
            console.log('get menus')
            return new Promise(async (resolve, reject) => {
                try {
                    const { data } = await axios.get(
                        `http://sslchkr.com/wp-json/menus/v1/menus/primary`, {
                        headers: {
                            'Content-Type': 'application/json'
                        }
                    }
                    )
                    commit('SET_MENU', data)
                    resolve(data)
                } catch (e) {
                    reject(e)
                }
            })
        },
        getPage({ commit, state }, payload) {
            console.log('get page')
            return new Promise(async (resolve, reject) => {
                try {
                    const { data } = await axios.get(
                        `http://sslchkr.com/wp-json/wp/v2/pages/${payload.id}`, {
                        headers: {
                            'Content-Type': 'application/json'
                        }
                    }
                    )
                    commit('SET_PAGE', data)
                    resolve(data)
                } catch (e) {
                    reject(e)
                }
            })
        },
        getPost({ commit, state }, payload) {
            console.log('get post')
            return new Promise(async (resolve, reject) => {
                try {
                    const { data } = await axios.get(
                        `http://sslchkr.com/wp-json/wp/v2/posts/${payload.id}`, {
                        headers: {
                            'Content-Type': 'application/json'
                        }
                    }
                    )
                    commit('SET_POST', data)
                    resolve(data)
                } catch (e) {
                    reject(e)
                }
            })
        },
    },
}

a page component... I am matching the route name to an item slug from the menu object, and using that item object_id to fetch the page object.

<template>
  <div class="page">
    <div>
      <h1>{{ page.title.rendered }}</h1>
    </div>
    <div v-html="page.content.rendered"></div>
  </div>
</template>

<script>
export default {
  name: "Page",
  computed: {
    menuItem() {
      return this.$store.state.menu.items.find(
        (item) => item.slug === this.$route.name
      );
    },
    page() {
      return this.$store.state.page;
    },
  },
  mounted() {
    this.$store.dispatch("getPage", { id: this.menuItem.object_id });
  },
};
</script>

and the nav component for completeness...

<template>
<ul id="menu-primary list-inline">
    <li
        v-for="item in menu.items"
        :key="item.ID"
        class="nav-item list-inline-item"
    >
        <router-link :to="slash(item.slug)" class="nav-link">{{
            item.title
        }}</router-link>
    </li>
</ul>
</template>

<script>
export default {
  name: "Nav",
  computed: {
    menu() {
      return this.$store.state.menu;
    },
  },
  methods: {
    slash(s) {
      return `/${s}`;
    },
  },
};
</script>

Edit to include main.js and App.vue

import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap/dist/js/bootstrap.js'
import 'vue-toastification/dist/index.css'

import { createApp } from 'vue'
import Toast, { POSITION } from 'vue-toastification'

import App from './App.vue'
import router from './router'
import store from './store'

let app = createApp(App)
app.use(store)
app.use(router)
app.use(Toast, { position: POSITION.TOP_CENTER })
app.mount('#app')

<template>
  <link rel="stylesheet" :href="theme" />
  <Nav />
  <div class="container-fluid">
    <div class="row padding-top">
      <div class="col-md-2"></div>
      <div class="col-md-8">
        <router-view :key="$route.path" />
      </div>
      <div class="col-md-2"></div>
    </div>
  </div>
</template>

<script>
import Nav from "@/components/Nav.vue";

export default {
  components: {
    Nav,
  },
  computed: {
    theme() {
      return this.$store.state.theme;
    },
  },
  mounted() {
    this.$store.dispatch("getTheme");
  },
};
</script>



Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source