Search code examples
vue.jsvuejs3vue-router

VueJs render function not re-rendering new dynamic component when it changes


Disclaimer: This problem is not exactly related to vue-router but it's easiest for me to demonstrate the issue by reference to vue-router. It's a core problem vue-router solves but I just need a solution to the same problem (maybe in a simpler alternative way). Don't "XY" me.

I'm trying to implement a very simple alternative to the vue-router plugin, for reasons ... Like vue-router I have the two components RouterLink and RouterView and some routing table. Clicking a RouterLink causes the plugin to use the lookup table to find a new component to render which is rendered by the ViewRouter.

The way I've implemented it, ViewRouter has a ref to the component it should render which is updated by the plugin middleware. Problem is, even though render() of the ViewRouter component is called and the component ref has a new value, the content actually rendered to the browser never changes. It's like Vue runtime decided it doesn't need to re-render. What am I doing wrong? How to solve?

Sample code:

import { type App, type Component, defineComponent, h, type PropType, shallowRef, ref } from 'vue';
import HomeView from './HomeView.vue';
import AboutView from './AboutView.vue';

const routes = [
  {
    path: '/',
    name: 'home',
    component: HomeView
  },
  {
    path: '/about',
    name: 'about',
    component: AboutView,
  }
];

const currentComponent = shallowRef<Component>(() => h('div', h('h1', 'Default')));

export function createRouter() {
    return (app: App) => {
      app.component('router-link', RouterLink);
      app.component('router-view', RouterView);
    };
}

function matchTo(to: string) {
  for(const route of routes) {
    if(route.path === to) {
      return route;
    }
  }
}

function setTo(to: string) {
  const route = matchTo(to);
  console.log('setComponentTo', to, route);
  if(!route) return;
  currentComponent.value = route.component;
}

export const RouterLink = defineComponent({
  name: 'RouterLink',
  props: {
    to: {
      type: String as PropType<string>,
      required: true,
    },
  },
  setup(props, { slots }) {
    return () => {
      return h(
        'a',
        {
          onClick: () => setTo(props.to),
        },
        slots.default && slots.default()
      );
    };
  },
});

export const RouterView = defineComponent({
  name: 'RouterView',
  setup() {
    const view = currentComponent.value ? h(currentComponent.value) : [];
    return () => {
      console.log('View render', (currentComponent.value as any).__file);
      return h('div', [view]);
    };
  }
});

Full demo project


Solution

  • You lose reactivity in the line with if. I don't really understand its meaning, but if you pass h(currentComponent.value) directly to h, the code starts working. Here is my version of RouterView:

        export const RouterView = defineComponent({
          name: 'RouterView',
          setup() {
            return () => {
              console.log('View render', (currentComponent.value as any).__file);
              return h('div', [h(currentComponent.value)]);
            };
          }
        });
    

    Or the ternary construct must be computed...