Search code examples
javascriptvue.jsvue-router

Vue Router resolve view change only when all components in view have fetched their required data


In Vue I have created complete SFC components which fetch, render and interact with data all on their own. I just have to pass it the respective API endpoint as a prop and the CRUD logic always stays the same. The data fetching is activated using onMounted and onBeforeRouteChange.

The component is empty until it has received a reply from the backend. I could display a loader while the data is fetching. However, if there are multiple components with loading animations and different load times, then that is bad UX and the user might get confused why the view keeps changing.

Therefore I would like to render the complete Router view only once all the data-fetching components have emitted some kind of ready status.

I know you could fetch the data in the navigation guards, e.g., beforeResolve, but all of the navigation guards (with afterEach being the exception) activate before the view component is mounted. This means that any logic in the view component starts happening only once it is visible.

My best case scenario would be:

  1. User clicks on a <RouterLink :to="nextView">.
  2. The page displays a global loading bar, the view doesn't change.
  3. All child components in nextView start fetching their data.
  4. Once all child components in nextView have fetched their data, the view gets rendered/changes.
  5. The loading bar disappears.

I have tried making all the data-fetching components emit some kind of ready status, then make the view component modify a global store to represent ready status once all of its' components are ready. I would then try to wait for that ready status in the beforeResolve navigation guard, but, again, beforeResolve does not mount the component, which means that it will never receive the status change.


Solution

  • There is no way to trigger component mounted hook or even to set its state without mounting it. Also, you cannot mount the next route component, while keeping rendered the previous route component.

    My suggestion is to connect a centralised store to each child component (You can connect to it also independently only passing the store identifier). This way you can populate the store before mounting the component and then connect to it afterwards.

    Now, you only need a way to tell the router guard which store to populate before navigating to a specific route. I suggest to set this information in route metadata, for example create an array of vuex namespaces on which a specific action should be triggered.

    router.beforeEach(async (to, from, next) => {
      if (to.meta && to.meta.childStores) {
        // setting a loading flag in a global vuex module to trigger the loader
        store.commit("setLoading", true);
        // Dispatching the actions in each vuex module required for this route 
        for (let s of to.meta.childStores) {
          await store.dispatch(`${s}/fetchData`);
        }
        // disabling the loader after all requests have finished and proceeding to the next route
        store.commit("setLoading", false);
        next();
      }
      store.commit("setLoading", false);
      next();
    });
    

    You can view this Codesandbox with a minimal working example.