Skip to content

Commit

Permalink
feat(loaders): add useIsDataLoading hook (#559)
Browse files Browse the repository at this point in the history
Co-authored-by: Eduardo San Martin Morote <[email protected]>
Co-authored-by: Eduardo San Martin Morote <[email protected]>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
  • Loading branch information
4 people authored Jan 15, 2025
1 parent 6d52071 commit 9c69f54
Show file tree
Hide file tree
Showing 3 changed files with 68 additions and 4 deletions.
6 changes: 5 additions & 1 deletion src/data-loaders/entries/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ export type {
export { toLazyValue } from '../createDataLoader'

// new data fetching
export { DataLoaderPlugin, NavigationResult } from '../navigation-guard'
export {
DataLoaderPlugin,
NavigationResult,
useIsDataLoading,
} from '../navigation-guard'
export type {
DataLoaderPluginOptions,
SetupLoaderGuardOptions,
Expand Down
30 changes: 30 additions & 0 deletions src/data-loaders/navigation-guard.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
DataLoaderPlugin,
NavigationResult,
DataLoaderPluginOptions,
useIsDataLoading,
} from 'unplugin-vue-router/data-loaders'
import { mockPromise } from '../../tests/utils'
import {
Expand Down Expand Up @@ -339,6 +340,35 @@ describe('navigation-guard', () => {
'does not call commit for a loader if the navigation is canceled by another loader'
)

it('sets isDataLoading within a navigation', async () => {
const { app } = setupApp({ isSSR: false })
const isGloballyLoading = app.runWithContext(() => useIsDataLoading())
expect(isGloballyLoading.value).toBe(false)

const router = getRouter()
const l1 = mockedLoader()
const l2 = mockedLoader()
router.addRoute({
name: '_test',
path: '/fetch',
component,
meta: {
loaders: [l1.loader, l2.loader],
},
})

router.push('/fetch')
await vi.runOnlyPendingTimersAsync()
expect(isGloballyLoading.value).toBe(true)

l1.resolve()
await vi.runAllTimersAsync()
expect(isGloballyLoading.value).toBe(true)
l2.resolve()
await vi.runAllTimersAsync()
expect(isGloballyLoading.value).toBe(false)
})

describe('signal', () => {
it('aborts the signal if the navigation throws', async () => {
setupApp({ isSSR: false })
Expand Down
36 changes: 33 additions & 3 deletions src/data-loaders/navigation-guard.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import { isNavigationFailure } from 'vue-router'
import { effectScope, type App, type EffectScope } from 'vue'
import {
effectScope,
inject,
shallowRef,
type InjectionKey,
type ShallowRef,
type App,
type EffectScope,
} from 'vue'
import {
ABORT_CONTROLLER_KEY,
APP_KEY,
Expand All @@ -19,6 +27,12 @@ import type {
import { type _Awaitable } from '../utils'
import { toLazyValue, type UseDataLoader } from './createDataLoader'

/**
* Key to inject the global loading state for loaders used in `useIsDataLoading`.
* @internal
*/
export const IS_DATA_LOADING_KEY = Symbol() as InjectionKey<ShallowRef<boolean>>

/**
* TODO: export functions that allow preloading outside of a navigation guard
*/
Expand All @@ -34,7 +48,7 @@ import { toLazyValue, type UseDataLoader } from './createDataLoader'
export function setupLoaderGuard({
router,
app,
effect,
effect: scope,
isSSR,
errors: globalErrors = [],
selectNavigationResult = (results) => results[0]!.value,
Expand Down Expand Up @@ -64,6 +78,10 @@ export function setupLoaderGuard({

router[IS_SSR_KEY] = !!isSSR

// global loading state for loaders used in `useIsDataLoading`
const isDataLoading = scope.run(() => shallowRef(false))!
app.provide(IS_DATA_LOADING_KEY, isDataLoading)

// guard to add the loaders to the meta property
const removeLoaderGuard = router.beforeEach((to) => {
// Abort any pending navigation. For cancelled navigations, this will happen before the `router.afterEach()`
Expand Down Expand Up @@ -156,6 +174,9 @@ export function setupLoaderGuard({

// unset the context so all loaders are executed as root loaders
setCurrentContext([])

isDataLoading.value = true

return Promise.all(
loaders.map((loader) => {
const { server, lazy, errors } = loader._.options
Expand All @@ -164,7 +185,7 @@ export function setupLoaderGuard({
return
}
// keep track of loaders that should be committed after all loaders are done
const ret = effect.run(() =>
const ret = scope.run(() =>
app
// allows inject and provide APIs
.runWithContext(() =>
Expand Down Expand Up @@ -225,6 +246,7 @@ export function setupLoaderGuard({
// unset the context so mounting happens without an active context
// and loaders do not believe they are being called as nested when they are not
setCurrentContext([])
isDataLoading.value = false
})
})

Expand Down Expand Up @@ -414,3 +436,11 @@ export interface DataLoaderPluginOptions {
*/
errors?: Array<new (...args: any) => any> | ((reason?: unknown) => boolean)
}

/**
* Return a ref that reflects the global loading state of all loaders within a navigation.
* This state doesn't update if `refresh()` is manually called.
*/
export function useIsDataLoading() {
return inject(IS_DATA_LOADING_KEY)!
}

0 comments on commit 9c69f54

Please sign in to comment.