posva / unplugin-vue-router

Next Generation file based typed routing for Vue Router
https://uvr.esm.is
MIT License
1.69k stars 82 forks source link

pendingLoad `Cannot read properties of null (reading 'then')` #495

Closed minht11 closed 2 months ago

minht11 commented 2 months ago

After upgrade to latest version of unplugin-router-router and using pinia colada, I noticed errors in sentry.

image image

I do not have a reproduction but I narrowed it down to this: https://github.com/posva/unplugin-vue-router/blob/fb772a28baf1c1daa6c801a246554cee9e29997e/src/data-loaders/defineLoader.ts#L345-L350

Promise is asserted as not being null, but at least from the Sentry errors that is not the case. Maybe it is race condition thing, most of the issues are from android, though I see some macs as well. I haven't seen it myself.

posva commented 2 months ago

If pendingLoad is null, you might be using loaders in an unexpected way. For example, using a loader in a component while it's not exported by the page rendering that component. A reproduction will really help here. Focus on the page where this happened and its components. The templates should be irrelevant.

posva commented 2 months ago

@minht11 I can't help if you don't share the code relevant to using data loaders. This would be helpful to provide runtime warnings in the future to help users detect problems

minht11 commented 2 months ago

I know, I haven't been able to reproduce it myself yet, but I patched the package to show more information when error occurs. This one does not have data

image

This one has data

image
// Send entry to allow debugging 
+    if (!entry.pendingLoad) {
+      throw new TypeError("Unknown error. Cannot read properties of null (reading 'then')", {
+        cause: stringifyCircularJSON({
+          staged: !!entry.staged,
+          error: !!entry.error.value,
+          isLoading: entry.isLoading.value,
+          ext: {
+            asyncStatus: entry.ext?.asyncStatus.value,
+            status: entry.ext?.status.value,
+          },
+          routePath: entry.route?.path,
+          routeName: entry.route?.name,
+          toPath: entry.to?.fullPath,
+          toName: entry.to?.name,
+          data: entry.data.value ? 'has data' : 'no data',
+          isPendingLoad: entry.pendingLoad ? 'is pending' : 'no pending',
+          isPendingTo: entry.pendingTo ? 'is pending' : 'no pending',
+        })
+      });
+    }

Is there anything I could log that could be helpfull?

posva commented 2 months ago

Yes, the code of the page for that route and the code of the components using any of the loaders that is rendered by that page

minht11 commented 2 months ago

I can't share the code, loaders are used in multiple components and levels, not one clean snippet, issue also occurs in multiple pages. Anyway I will still try to narrow it down, I thought logging some information when error occurs, like if for some reason loader wasn't staged (just a random example, could help.

posva commented 2 months ago

Anyway I will still try to narrow it down

Yes, please.

minht11 commented 2 months ago

Took a lot of time, but I managed to reproduce it.

Issue occurs when one of the loaders throws an error and you try to navigate to same page again.

// App.vue
<script lang="ts" setup>
import { ref } from 'vue';
import { RouterView, useRouter } from 'vue-router';

const router = useRouter();
const isError = ref(false);
router.onError(() => {
  isError.value = true;
});

const reset = () => {
  isError.value = false;
  router.push('/');
};
</script>

<template>
  <div v-if="isError">
    Error happened
    <button @click="reset">Login</button>
  </div>
  <div v-else>
    <RouterView />
  </div>
</template>
// pages/index.vue
<script lang="ts">
import { defineBasicLoader } from 'unplugin-vue-router/data-loaders/basic';
import { useRouter } from 'vue-router';

export const useHomePageData = defineBasicLoader(() => Promise.resolve(true));
</script>

<script lang="ts" setup>
useHomePageData();

const router = useRouter();
const details = () => router.push({ name: '/details' });
</script>

<template>
  <button @click="details">Go to details</button>
</template>
// pages/details.vue
<script lang="ts">
import { defineBasicLoader } from 'unplugin-vue-router/data-loaders/basic';

export const useDetailsPageData = defineBasicLoader(async () => {
  throw new Error('Not implemented');
});
</script>

<script lang="ts" setup>
// Empty
</script>

<template>Details Page</template>

https://github.com/user-attachments/assets/d66dcef2-4cfe-4610-945a-fd1a4cc84706

posva commented 2 months ago

Thanks! the online repro is great, no need to paste the code here too when you do that 😄 It seems like the conditional display of the RouterView is related to the bug, if you remove it, it works. Maybe that helps you out in the meantime.

posva commented 2 months ago

In your case, the issue came down to having the duplicated navigation of /, if you avoid it, you also avoid the problem.

minht11 commented 2 months ago

Yeah duplicated navigation is the cause, we saw in few more cases, not only with errors, but also when clicking the link of the active page on sidebar and later navigating somewhere else would produce an error, though it had some before enter guards and nested layouts.

Workaround for that one was just hiding the link, other cases like the one in the repo are trickier, because what should you do after error happens if you can't navigate.