Understanding Nuxt & Vue hooks and lifecycle (part 2)

8 minute read

This is part 2 of mini-series - Understanding Nuxt & Vue hooks and lifecycle. You can start with Part 1 here, to make sure you are at least vaguely familiar with most of the required concepts. If you have other programming background, but not in Vue/Nuxt, you might also find my other post useful.

What’s in the app

The sample code contains very simple examples of all the mechanisms/hooks discussed in Part 1. For this article to make sense, especially “How it all works” section, you will need to download it and run locally to follow along.

Noteworthy files:

  • LinksComponent.vue
    • Contains a (hardcoded) list of various links in the project to allow user-navigation.
    • Includes mixin logRouteQueryAndParams.js. It demonstrates that what’s in mixin (computed property routeParams) is executed in a same way as if it was directly defined in the component, and that mixin code has access to this.
    • Shows most of the Vue component lifecycle methods
  • globalMiddleware.js and localMiddleware.js - as the names suggest, global middleware is attached from nuxt.config.js and thus executed before every route, whereas local middleware is only included for test1/_param1/_param2 route.
  • A few routes (pages):
    • index.vue - the starting point, contains LinksComponent
    • test1/param1?/param2? - a route with two optional parameters, meaning that test1/, test1/lorem and test1/lorem/ipsum all would land on the page generated by code in _param2.vue file
    • test1/param1?/param2? - a route equivalent to test1 route, it shows that if you don’t like naming your vue files with the name of last parameter to the route, you can name place them in subdirectory and name them index.vue instead
    • foo/x/_id? and foo/y/_id? - shows how dynamic nested routes work. A nested route is where a page contains another router-view component, like in foo.vue. You always get one by default with Nuxt (you don’t explicitly include it, Nuxt does it for you), so this is effectively a router-inside-router. Hence the name, nested.

How does it all work?

Let’s assume the user first navigates to our main page (e.g. http://localhost:3000) followed by navigating to various other pages, by clicking appropriate links. You need to click on the links rather than put URLs directly in the browser if you want to observe SPA mode in action. This is because navigating from address bar would force SSR mode.

Let’s take a look at an example user journey:

(first visit) http://localhost:3000

What’s in the logs?

On the server, before answer returned to client:

(AlternativeEventBus Plugin) SSR: true inject component with id: 4
(NuxtServerInit) SSR: true
(Global Middleware) SSR: true
(LinksComponent) SSR: true [BeforeCreate]
(LinksComponent) SSR: true [Created] SampleProp: Prop from main page, SampleData: Lorem Ipsum Data
(LinksComponent) Created Refs:

On the client (browser) side:

(EventBus Plugin) SSR: false inject component with id: 1
(AlternativeEventBus Plugin) SSR: false inject component with id: 2
(LinksComponent) SSR: false [BeforeCreate]
(LinksComponent) SSR: false [Created] SampleProp: Prop from main page, SampleData: Lorem Ipsum Data
(LinksComponent) Created Refs:
(LinksComponent) SSR: false [Mounted] SampleProp: Prop from main page, SampleData: Lorem Ipsum Data
(LinksComponent) Mounted Refs: Foo With No Params,Foo X With Param1,(...)

What just happened?

  • globalMiddleware is only executed in SSR in this call
  • AlternativeEventBus Plugin is setup on both sides (client and server)
  • EventBus Plugin is only setup on the client
  • beforeCreate and created are called on both server and client
  • Mounted is only called on the client
  • this.$refs are only populated in Mounted

Where did we go wrong?

Imagine you have code somewhere in middleware or fetch and you register this.$eventBus.$on event listener. Then, based on some user interaction, you dispatch an event via this.$eventBus.$emit. Things don’t work, listener is not called - why?

As you might notice, AlternativeEventBus Plugin id is different on client and server (if this is not the case for you, refresh the page, as ID on the server will change on subsequent SSR calls). That’s because this plugin’s code is executed on both client and server, and both sides create an object. Middleware and fetch are only executed in SRR on first call, so your listener is registered on the SSR instance of eventBus. The client-interaction code runs in browser, so your emit event triggers on the client-side instance of eventBus. Not the same instance - communication does not happen.

The main usage for eventBus is for one part of the code to notify another that something happened. This allows us to write a more decoupled application. For example your login code could publish an event that a user has just logged in, so that other parts of code can react and fetch extra user data into VueX. This way login code itself does not need to know anything about user data required in other parts of the app.

Therefore, in reality, such eventBus plugin may not make sense in dual (SSR/client) mode. If interaction always happens in the browser, it makes more sense to make such plugin client-side only. This way, if somebody tries to register a listener to event in SSR code, they will get a nice error saying that eventBus is undefined - rather than allowing them to register on an instance that will never receive any events.

What’s in the logs?

In this, and all the following calls everything happens on the client (browser) side only :

(Global Middleware) SSR: false
(Local Middleware) SSR: false
(Mixin) /test1/val1AsyncData: {"param1":"val1"}
(Mixin) /test1/val1Fetch: {"param1":"val1"}
(LinksComponent) SSR: false [BeforeCreate]
(LinksComponent) SSR: false [Created] SampleProp: Test1, SampleData: Lorem Ipsum Data
(LinksComponent) Created Refs: 
(LinksComponent) SSR: false [Mounted] SampleProp: Test1, SampleData: Lorem Ipsum Data
(LinksComponent) Mounted Refs: Foo With No Params,Foo X With Param1,(...)

What just happened?

  • Global Middleware, and now also local middleware, are processed on the client.
  • Mixin code from logRouteQueryAndParams for fetch and asyncData is now called
  • all Vue lifecycle hooks from LinksComponent are called again. The route has changed and the instance of LinksComponent that was used in index.vue would now be destroyed and a new one (for test1 route) created

Where did we go wrong?

Fetch and asyncData was not called on home page but it was on this page, why? That’s because index.vue does not include it as mixin, and _param2.vue does. LinksComponent does contain this mixin too, but asyncData and fetch are not called for components. If you have a situation where your data does not seem to populate your UI, always double check if your fetching code is in a page, not in a component.

What’s in the logs?

(Global Middleware) SSR: false
(Mixin) /test2/val1/val2AsyncData: {"param1":"val1","param2":"val2"}
(Mixin) /test2/val1/val2Fetch: {"param1":"val1","param2":"val2"}
(LinksComponent) SSR: false [BeforeCreate]
(LinksComponent) SSR: false [Created] SampleProp: Test32, SampleData: Lorem Ipsum Data
(LinksComponent) Created Refs: 
(LinksComponent) SSR: false [Mounted] SampleProp: Test2, SampleData: Lorem Ipsum Data
(LinksComponent) Mounted Refs: Foo With No Params,Foo X With Param1,(...)

What just happened?

  • Global Middleware is processed on the client. Local is not as it was not attached to this route.
  • Mixin code from logRouteQueryAndParams for fetch and asyncData is now called.
  • all Vue lifecycle hooks from LinksComponent are called

What’s in the logs?

(Global Middleware) SSR: false
(Mixin) /foo/x/val1AsyncData: {"id":"val1"}
(Mixin) /foo/x/val1Fetch: {"id":"val1"}
(Mixin) /foo/x/val1AsyncData: {"id":"val1"}
(Mixin) /foo/x/val1Fetch: {"id":"val1"}
(LinksComponent) SSR: false [BeforeCreate]
(LinksComponent) SSR: false [Created] SampleProp: SampleProp from Foo, SampleData: Lorem Ipsum Data
(LinksComponent) Created Refs: 
(LinksComponent) SSR: false [Mounted] SampleProp: SampleProp from Foo, SampleData: Lorem Ipsum Data
(LinksComponent) Mounted Refs: Foo With No Params,Foo X With Param1,(...)

What just happened?

  • Global Middleware is processed on the client.
  • Mixin code from logRouteQueryAndParams for fetch and asyncData is now called - TWICE! This is because both foo.vue, and foo/x/_id.vue include the mixin, and both are pages. In reality, you wouldn’t have the same fetch (from mixin) included in parent and nested route, so the fetch/asyncData would not be doing the same thing.
  • all Vue lifecycle hooks from LinksComponent are called

What’s in the logs?

(Global Middleware) SSR: false
(Mixin) /foo/y/val1AsyncData: {"id":"val1"}
(Mixin) /foo/y/val1Fetch: {"id":"val1"}

What just happened?

  • Oh dear! Why is this output so different than for Foo X? This is because we are navigating within a nested route now. The app is smart enough to know that the shell (foo.vue) has not changed between foo/x/val1 and foo/y/val1 - it’s only the nested part (x/_id.vue vs y/_id.vue) that has changed. Therefore, there is no point regenerating anything related to foo.vue. We only execute what’s specific to y/_id.vue - and this file does not contain a separate LinksComponent, so does not run its lifecycle methods.
  • Global Middleware is still processed on the client.
  • Mixin code from logRouteQueryAndParams for fetch and asyncData is now called - but only for foo/y/_id.vue

Where did we go wrong?

We completely misunderstood/didn’t even read what nested components are, so at one point we had a structure like in foo route, but foo.vue page did not include <router-view>. The routing was working fine, but the fetch was then only called for route change - not params change. For example, if you went from /foo/x/1 to /foo/x/2 - the fetch for /foo/x/2 would not be called. But if you went from /foo/x/1 to /test1 and then to /foo/x/2, then fetch is called.

If you are in similar situation, and for some reason you actually need to make some changes in foo.vue data, then your best option is to add watch on route, i.e.:

watch: {
    '$route'(to, from) {
        // whatever you need to refresh goes here
        // you can get route (URL) params and query arguments before and after from `to` and `from` method parameters
    }
}

Play yourself!

I hope the above go-through makes sense - but nothing will be as enlightening as taking this example project and playing with it yourself. Add hooks, extend existing code, navigate through the app and observe what happens. Let me know if anything is unclear!

In the last part, coming up soon, I’ll summarise both parts with a short neat table - stay tuned!

Comments