😲VueJS pages with Dynamic layouts! Problems and a solution!

Michael "lampe" Lazarski - Feb 8 '20 - - Dev Community

I'm currently working on a big Progressive Web App (PWA) for a client. For the frontend, we use VueJS with the Vue Router, VueX, and some more VueJS packages.

We started with two layouts. One layout is a Modal Layout Where you have a login or register form. So everything that is in that layout is in the vertical and horizontal center of the page. Our second layout is the layout for your typical app. This layout contains our components like a navigation menu, Notifications, search, and so on.

We are also using VueX and Axios to fetch data from our backend. We don't need to pass props from top to bottom or the other way around. We have stores that model the backend data and methods if needed.

Now that you have a fundamental overview of the technologies used, I will discuss some problems with the commonly found solutions for dynamic layouts in VueJS.

Intro

For the code examples, I created 3 vue cli projects.

All of them have the following code snippet added in the main.js file.

Vue.mixin({
  created() {
    console.log('[created] ' + this.$options.name)
  },
});

This will conols.log() the Component name everytime a component is created. This is an easy way to see how your VueJS components are created. You can also add mounted() and detroyed() hooks. For our experiment created() is enough.

Problem 1: Rerendering on route change

When you search online for dynamic layouts, you will find a lot of solutions, and one of the most common is the following one.

In your App.vue you have the following code:

<template>
  <div id="app">
    <router-view/>
  </div>
</template>

And then you tell every page/view what layout it should have. It usually looks like the following About.vue component.

<template>
  <LayoutB>
    <div class="about">
      <h1>This is an about page</h1>
    </div>
  </LayoutB>
</template>

<script>
import LayoutB from "../layouts/LayoutB";
export default {
  name: "About",
  components: {
    LayoutB
  }
};
</script>

This will work, and you will not see any problems with fast machines and because we don't do much on that page.

So what is the problem? For this, we now look at our nifty Vue.Mixin() helper function.

The console.log should look like this:

Alt Text

We can see that if we load the page, we see the following creation order.

'App (entry point)' -> 'Home (view/page)' -> 'LayoutA (layout)' -> 'Components'

If we look at how we have set up our components right now, then this is correct. Loading the Page before the layout can lead to problems, but it is not such a significant performance hit.

The bigger problem is the following:
Alt Text

We are destroying the complete layout and creating it again. This will lead to a sluggish UI/UX and defeats the purpose of having all these components separated. If we destroy and create them even if we don't have to.

This gets even worse if you have a notification system where you create listeners every time you change the page.

This solution is not very satisfying even it kind of works.

Problem 2: Double rendering

This is probably the most popular solution I found in several tutorials and StackOverflow answers.

We change our App.vue code to:

<template>
  <div id="app">
    <component :is="layout">
      <router-view :layout.sync="layout" />
    </component>
  </div>
</template>

<script>
export default {
  name: "App",
  data() {
    return {
      layout: "div"
    };
  }
};
</script>

and our About.vue to the following code

<template>
  <div class="about">
    <h1>This is an about page</h1>
  </div>
</template>

<script>
import LayoutB from "../layouts/LayoutB";
export default {
  name: "About",
  created() {
    this.$emit("update:layout", LayoutB);
  }
};
</script>

The most significant change here is the sync and $emit functionality. What we have now done we moved the layout out to the App.vue component, and the view/page component will tell the App.vue what layout to load.

Again just by looking at the browser, you will see that this solution works! Now Let's have a look at our console.log() output.

Alt Text

App (entry point) -> 'Home (view/page)' -> 'LayoutA (layout)' -> 'Components' -> 'Home (view/page) again😱 -> Click on Contact link ->'Contact (view/page)

We solved one problem. Now the layout does not get destroyed and created again on every route change, but we also created a new problem!

Every time a new Layout gets rendered, the page/view in it gets created then destroyed than created again. This can lead to problems with our stores.

When you have a fetch() function in your component to load a list, this fetch() function will ping the server twice instead of just once. Now imagine your backend has no caching, and a heavy calculation is running twice!

Also, if your store does not check if you are getting duplicated data, you will see everything twice on that list.

And again, in our example, Home is rendered before the LayoutA.

This is just one problem you see that can happen with this solution.

That method is also not an excellent solution to our problem.

The Solution: Using the meta object in our route

We need to change our App.vue again.

<template>
  <div id="app">
    <component :is="this.$route.meta.layout || 'div'">
      <router-view />
    </component>
  </div>
</template>

<script>
export default {
  name: "App",
};
</script>

Our About.vue now looks like this

<template>
  <div class="about">
    <h1>This is an about page</h1>
  </div>
</template>

<script>
export default {
  name: "About"
};
</script>

So the Page does not know in what Layout it is rendered.
But where is this information now stored?
In our router/index.js file!

import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
import About from '../views/About.vue'
import Contact from '../views/Contact.vue'
import LayoutA from '../layouts/LayoutA.vue'
import LayoutB from '../layouts/LayoutB.vue'
Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home,
    meta: { layout: LayoutA }
  },
  {
    path: '/about',
    name: 'About',
    component: About,
    meta: { layout: LayoutB }
  },
  {
    path: '/contact',
    name: 'contact',
    component: Contact,
    meta: { layout: LayoutA }
  }
]

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
})

export default router

The most important line here is meta: { layout: LayoutA } in every route definition.

Let's have a look again at our console.log() output.

Alt Text

App (entry point) -> LayoutA (layout) -> Components from the Layout -> Home (view/page)

Now, this looks good. We finally have the right order and no double rendering.
Also, we can change the route without destroying and creating the Layout even if it does not have to change.

After implementing this solution, we could feel that the app was smoother and just felt better. Even with your eye, you could not see it. Just the smoothness alone was a big plus.

Also, not hammering our server with unneeded requests! We could lower some limits on our API endpoints.

This small fix was a win for everybody from the end-user to the stakeholders to the actual developers.

Git Repo with the code

I created a repo where you can find the two problematic projects and the solution we went with

LINK

**If you liked this content, please click the heart or the unicorn!

If you want to read it later, click the bookmark button under the unicorn!**

👋Say Hello! Instagram | Twitter | LinkedIn | Medium | Twitch | YouTube

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player