DEV Community

Cover image for 😲VueJS pages with Dynamic layouts! Problems and a solution!
Michael "lampe" Lazarski
Michael "lampe" Lazarski

Posted on

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

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)
  },
});


Enter fullscreen mode Exit fullscreen mode

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>


Enter fullscreen mode Exit fullscreen mode

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>


Enter fullscreen mode Exit fullscreen mode

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>


Enter fullscreen mode Exit fullscreen mode

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>


Enter fullscreen mode Exit fullscreen mode

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>


Enter fullscreen mode Exit fullscreen mode

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>


Enter fullscreen mode Exit fullscreen mode

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


Enter fullscreen mode Exit fullscreen mode

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

Top comments (23)

Collapse
 
dinsmoredesign profile image
Derek D • Edited

Interesting. I've thought about doing it the way you describe here, but then discovered you can also use parent routes with a blank path, which is far more explicit IMO, ie:

routes: [

    {
        path: '',
        name: 'LayoutA',
        component: LayoutA,
        children: [
            {
                path: '/1',
                name: 'page1',
                component: Page1
            },
            {
                path: '/2',
                name: 'page2',
                component: Page2
            }
        ]
    },
    {
        path: '',
        name: 'LayoutB',
        component: LayoutB,
        children: [
            {
                path: '/3',
                name: 'page3',
                component: Page3
            },
            {
                path: '/4',
                name: 'page4',
                component: Page4
            }
        ]
    }

]
Enter fullscreen mode Exit fullscreen mode

Now I'm curious to see if there's any performance benefits to switching. Will have to test!

Collapse
 
zulu001 profile image
Dixon Etta-Ekuri

I use a similar setup for my projects too.
But i tend to split my routes in 2 other files, then import them into the main router file.

that way it's easy to manage routing configuration without actually messing with the other parts of the app.

Collapse
 
lampewebdev profile image
Michael "lampe" Lazarski

Yeah, in general, it is the same solution as the one I chose to go with.

I'm not sure if I like the empty path.

Also in our big app, we don't just have one router file.

We have a router file for each path.

For example /someSite/ would have its own router file with the paths for it like someSite/ someSite/:id someSite/new etc.

I would like to see the create output for your solution :)

Collapse
 
jjstanton profile image
jjstanton

Thank you for this solution! It is very neat and elegant. Has made my app a lot smoother.

I'm using Vue 3 along with router 4.0.0 for this and got it to work flawlessly when all the routes are in a single file but when I break the routes into separate files I am unable to get child routes to work with your layout method. Have you encountered this before?

Collapse
 
subashcs profile image
subashcs

I am currently doing the same . I have the same question as yours , are there any benefit switching the structure you mentioned with the one described in the blog?

Collapse
 
savkataras profile image
Taras Savka • Edited

You should simply create a reusable component with slot. Import it globally and just use it that way. You won't need to link specific layout on each view.

Something like this, depending on your design complexity.

Export default {
name: 'layoyt',
}

Collapse
 
lampewebdev profile image
Michael "lampe" Lazarski

🤔🤔 and how does this support multiple different layouts?

Collapse
 
savkataras profile image
Taras Savka

Create few global components, apply different scss and you can have multiple layouts

Thread Thread
 
lampewebdev profile image
Michael "lampe" Lazarski

I'm not sure if you have read the article.

It is about how to load that layouts not how you implement a single layout 🤔🤔🤔🤔

Collapse
 
loast profile image
AlexYalinc

Does nuxt.js have the same problem?

Collapse
 
lampewebdev profile image
Michael "lampe" Lazarski

I did not test nuxt.js

As far as I know, they have a layout system extra implemented for nuxt.js.

Would be nice to see how they are doing it.

Collapse
 
artilishes profile image
Arthur

You should definitely give nuxt a try.
It's built to take out all the hassle you're currently doing manually. :)
In nuxt you simply create a layout in the layout folder, and tell pages what layout to use. Done.

Collapse
 
joellau profile image
Joel Lau

interesting article! thanks for writing this.

I'd just like to ask if there was any way to check on the performance gained by implementing things in this new method

Collapse
 
lampewebdev profile image
Michael "lampe" Lazarski

Hey thank you :)

Sadly we did not compare before and after.

We needed to fix it fast since it was at the end of a sprint. I just wanted to solve the problems with the other implementations.

You could feel and see the performance from not beeing 30fps every was to having smooth animations with 30fps+ everywhere.

Also, I would need to ask if I can show these metrics in public since this project is for a client.

In general, if I see some performance issues in a small part of the app I use the performance tool from firefox or chrome and dig into the charts and output since you can see what caused what in detail.

Collapse
 
joellau profile image
Joel Lau

wow, thank you for such a detailed response!

and yes, i can imagine the difference between < 30 fps and > 30fps being quite noticeable even without hard numbers :P

Collapse
 
safiullahsarhandi profile image
safiullahsarhandi

I think the simplest way to declare dynamic layout is in vue-router. Did you tested it? I always prefer to use children key of vue router to evaluate layouts for different dynamic pages

Collapse
 
lampewebdev profile image
Michael "lampe" Lazarski

Nope I did not test it.

I'm just not the biggest fan of the notation.

It should be fine.

Collapse
 
safiullahsarhandi profile image
safiullahsarhandi

You would love it.

Collapse
 
alam487 profile image
Alam487

Hi sir. I am trying to built a multistep form wizard in vue.js using Django where I have two flows one is personal user and other one is business user. So in personal user I have to prompt a form based on category selection. Like when we enter a category in html input field based on that category I have to show next form. Suppose if I enter a "IT" in category field then based on IT i have prompt next form related with IT fields.
So here first flow I have completed like displaying the form based on category selection. And the main problem is with business user here while we enter a category "IT" in business flow so at that time by default we are having next form with two radio buttons one is personal user radio button and other is business user so if we click on business user radio button then user are able to update his account from personal to business using registration form.
But while we click on personal radio button then I need to follow the same process of entering category and showing the next form based on this category enter by user but i was unable to achieve this. So here i have stuck and need help

Collapse
 
oxavibes profile image
Stefano

Pretty neat solution Michael! Thanks for sharing

Collapse
 
ztheleader profile image
Zain Mohsin

Thank you for the informative article. One small thing though:

<component :is="this.$route.meta.layout || 'div'">
<component :is="$route.meta.layout || 'div'"> ✔️

Collapse
 
pjammula profile image
PJ

Thank you Micheal for the post!
I was struggling with multiple rendering issue mentioned in 2nd solution and tried out your solution with meta object.

It worked like charm!

Collapse
 
enderjchacon profile image
Ender Chacon

Crack