@tanstack/vue-router
Version:
Modern and scalable routing for Vue applications
207 lines • 8.07 kB
JSX
import * as Vue from 'vue';
import { getLocationChangeInfo, trimPathRight } from '@tanstack/router-core';
import { isServer } from '@tanstack/router-core/isServer';
import { batch, useStore } from '@tanstack/vue-store';
import { useRouter } from './useRouter';
import { usePrevious } from './utils';
// Track mount state per router to avoid double-loading
let mountLoadForRouter = { router: null, mounted: false };
/**
* Composable that sets up router transition logic.
* This is called from MatchesContent to set up:
* - router.startTransition
* - router.startViewTransition
* - History subscription
* - Router event watchers
*
* Must be called during component setup phase.
*/
export function useTransitionerSetup() {
const router = useRouter();
// Skip on server - no transitions needed
if (isServer ?? router.isServer) {
return;
}
const isLoading = useStore(router.stores.isLoading, (value) => value);
// Track if we're in a transition - using a ref to track async transitions
const isTransitioning = Vue.ref(false);
// Track pending state changes
const hasPending = useStore(router.stores.hasPending, (value) => value);
const previousIsLoading = usePrevious(() => isLoading.value);
const isAnyPending = Vue.computed(() => isLoading.value || isTransitioning.value || hasPending.value);
const previousIsAnyPending = usePrevious(() => isAnyPending.value);
const isPagePending = Vue.computed(() => isLoading.value || hasPending.value);
const previousIsPagePending = usePrevious(() => isPagePending.value);
// Implement startTransition similar to React/Solid
// Vue doesn't have a native useTransition like React 18, so we simulate it
// We also update the router state's isTransitioning flag so useMatch can check it
router.startTransition = (fn) => {
isTransitioning.value = true;
// Also update the router state so useMatch knows we're transitioning
try {
router.stores.isTransitioning.set(true);
}
catch {
// Ignore errors if component is unmounted
}
// Helper to end the transition
const endTransition = () => {
// Use nextTick to ensure Vue has processed all reactive updates
Vue.nextTick(() => {
try {
isTransitioning.value = false;
router.stores.isTransitioning.set(false);
}
catch {
// Ignore errors if component is unmounted
}
});
};
// Execute the function synchronously
// The function internally may call startViewTransition which schedules async work
// via document.startViewTransition, but we don't need to wait for it here
// because Vue's reactivity will trigger re-renders when state changes
fn();
// End the transition on next tick to allow Vue to process reactive updates
endTransition();
};
// Vue updates DOM asynchronously (next tick). The View Transitions API expects the
// update callback promise to resolve only after the DOM has been updated.
// Wrap the router-core implementation to await a Vue flush before resolving.
const originalStartViewTransition = router.__tsrOriginalStartViewTransition ??
router.startViewTransition;
router.__tsrOriginalStartViewTransition =
originalStartViewTransition;
router.startViewTransition = (fn) => {
return originalStartViewTransition?.(async () => {
await fn();
await Vue.nextTick();
});
};
// Subscribe to location changes
// and try to load the new location
let unsubscribe;
Vue.onMounted(() => {
unsubscribe = router.history.subscribe(router.load);
const nextLocation = router.buildLocation({
to: router.latestLocation.pathname,
search: true,
params: true,
hash: true,
state: true,
_includeValidateSearch: true,
});
// Check if the current URL matches the canonical form.
// Compare publicHref (browser-facing URL) for consistency with
// the server-side redirect check in router.beforeLoad.
if (trimPathRight(router.latestLocation.publicHref) !==
trimPathRight(nextLocation.publicHref)) {
router.commitLocation({ ...nextLocation, replace: true });
}
});
// Track if component is mounted to prevent updates after unmount
const isMounted = Vue.ref(false);
Vue.onMounted(() => {
isMounted.value = true;
if (!isAnyPending.value) {
if (router.stores.status.get() === 'pending') {
batch(() => {
router.stores.status.set('idle');
router.stores.resolvedLocation.set(router.stores.location.get());
});
}
}
});
Vue.onUnmounted(() => {
isMounted.value = false;
if (unsubscribe) {
unsubscribe();
}
});
// Try to load the initial location
Vue.onMounted(() => {
if ((typeof window !== 'undefined' && router.ssr) ||
(mountLoadForRouter.router === router && mountLoadForRouter.mounted)) {
return;
}
mountLoadForRouter = { router, mounted: true };
const tryLoad = async () => {
try {
await router.load();
}
catch (err) {
console.error(err);
}
};
tryLoad();
});
// Setup watchers for emitting events
// All watchers check isMounted to prevent updates after unmount
Vue.watch(() => isLoading.value, (newValue) => {
if (!isMounted.value)
return;
try {
if (previousIsLoading.value.previous && !newValue) {
router.emit({
type: 'onLoad',
...getLocationChangeInfo(router.stores.location.get(), router.stores.resolvedLocation.get()),
});
}
}
catch {
// Ignore errors if component is unmounted
}
});
Vue.watch(isPagePending, (newValue) => {
if (!isMounted.value)
return;
try {
// emit onBeforeRouteMount
if (previousIsPagePending.value.previous && !newValue) {
router.emit({
type: 'onBeforeRouteMount',
...getLocationChangeInfo(router.stores.location.get(), router.stores.resolvedLocation.get()),
});
}
}
catch {
// Ignore errors if component is unmounted
}
});
Vue.watch(isAnyPending, (newValue) => {
if (!isMounted.value)
return;
try {
if (!newValue && router.stores.status.get() === 'pending') {
batch(() => {
router.stores.status.set('idle');
router.stores.resolvedLocation.set(router.stores.location.get());
});
}
// The router was pending and now it's not
if (previousIsAnyPending.value.previous && !newValue) {
const changeInfo = getLocationChangeInfo(router.stores.location.get(), router.stores.resolvedLocation.get());
router.emit({
type: 'onResolved',
...changeInfo,
});
}
}
catch {
// Ignore errors if component is unmounted
}
});
}
/**
* @deprecated Use useTransitionerSetup() composable instead.
* This component is kept for backwards compatibility but the setup logic
* has been moved to useTransitionerSetup() for better SSR hydration.
*/
export const Transitioner = Vue.defineComponent({
name: 'Transitioner',
setup() {
useTransitionerSetup();
return () => null;
},
});
//# sourceMappingURL=Transitioner.jsx.map