Vue-router Keepalive
When using vue-router (v4) for your Single-Page-Application, one of the options is
to combine it with the <keep-alive>
tag. However, this may break your page components in unexpected ways, if done naively.
A typical usage of keep-alive
with vue-router
looks like this:
<router-view v-slot="{Component}">
<keep-alive>
<component :is="Component" />
</keep-alive>
</router-view>
And the recommended way to fetch data after navigation looks like this:
const route = useRoute()
watch(
() => route.params.id,
fetchTheDataHandler
)
But beware, what if you have another component that is also using the id
param. Your components will actually react to changes
in the id
that is not really meant for them. Even if components that are deactivated (not shown, but kept alive) seem to have their
watcher disabled, this does not work perfectly on component transitions. And {flush: 'post'}
does not help here.
So supose you have two pages, user/foo
and product/bar
(where foo
and bar
are id
parameters). Let’s start with
the user
page loaded, then navigate to product
. Then press back. Two things will happen. product
page will try to load its
data, because the watcher sees id
changing from bar
to foo
, before it gets deactivated. Meanwhile the user
page sees an
id
change after getting activated. So you loose state on both your components and trigger a data fetch. Not good, not what
you had in mind when trying to use <keep-alive>
.
There are some solutions floating around, but I think the correct one is to:
- Watch only changes if the route matches current component
- Only react to changes if the value actually changes
For that, I wrote a helper for composition API, that can be used like this:
watchRoute((r) => r.params.id, fetchDatahandler);
The helper takes care of checking the current component and checking if the value actually did change (in a deep comparison sense).
type RouteWatcher<T> = (l: RouteLocationNormalizedLoaded) => T;
type RouteWatchEffect<T> = (l: T) => void;
export function watchRoute<T>(
watcher: RouteWatcher<T>, handler: RouteWatchEffect<T>
): RouteLocationNormalizedLoaded
{
const route = useRoute();
const componentType = getCurrentInstance()?.type;
const routeComponentType = () => {
const matched = route.matched[0];
if (matched && matched.components) {
return matched.components['default'];
}
};
let lastParams: T | undefined = undefined;
watch([() => watcher(route), routeComponentType], ([params, type]) => {
if (type == componentType) {
if (!lodash.isEqual(lastParams, params)) {
lastParams = params;
handler(params);
}
}
}, {immediate: true});
return route;
}