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:
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:
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.
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.
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
**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)
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:
Now I'm curious to see if there's any performance benefits to switching. Will have to test!
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.
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 likesomeSite/
someSite/:id
someSite/new
etc.I would like to see the
create
output for your solution :)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?
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?
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',
}
🤔🤔 and how does this support multiple different layouts?
Create few global components, apply different scss and you can have multiple layouts
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 🤔🤔🤔🤔
Does nuxt.js have the same problem?
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.
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.
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
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.
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
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
Nope I did not test it.
I'm just not the biggest fan of the notation.
It should be fine.
You would love it.
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
Pretty neat solution Michael! Thanks for sharing
Thank you for the informative article. One small thing though:
<component :is="this.$route.meta.layout || 'div'">
❌<component :is="$route.meta.layout || 'div'">
✔️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!
Crack