feat(nuxt): add useRuntimeHook composable (#29741)

This commit is contained in:
Damian Głowala 2024-11-02 23:25:05 +01:00 committed by Daniel Roe
parent 09885b87ef
commit 59c28d5a5a
No known key found for this signature in database
GPG Key ID: CBC814C393D93268
7 changed files with 103 additions and 2 deletions

View File

@ -0,0 +1,43 @@
---
title: useRuntimeHook
description: Registers a runtime hook in a Nuxt application and ensures it is properly disposed of when the scope is destroyed.
links:
- label: Source
icon: i-simple-icons-github
to: https://github.com/nuxt/nuxt/blob/main/packages/nuxt/src/app/composables/runtime-hook.ts
size: xs
---
::important
This composable is available in Nuxt v3.14+.
::
```ts [signature]
function useRuntimeHook<THookName extends keyof RuntimeNuxtHooks>(
name: THookName,
fn: RuntimeNuxtHooks[THookName] extends HookCallback ? RuntimeNuxtHooks[THookName] : never
): void
```
## Usage
### Parameters
- `name`: The name of the runtime hook to register. You can see the full list of [runtime Nuxt hooks here](/docs/api/advanced/hooks#app-hooks-runtime).
- `fn`: The callback function to execute when the hook is triggered. The function signature varies based on the hook name.
### Returns
The composable doesn't return a value, but it automatically unregisters the hook when the component's scope is destroyed.
## Example
```vue twoslash [pages/index.vue]
<script setup lang="ts">
// Register a hook that runs every time a link is prefetched, but which will be
// automatically cleaned up (and not called again) when the component is unmounted
useRuntimeHook('link:prefetch', (link) => {
console.log('Prefetching', link)
})
</script>
```

View File

@ -38,3 +38,4 @@ export { useRequestURL } from './url'
export { usePreviewMode } from './preview'
export { useId } from './id'
export { useRouteAnnouncer } from './route-announcer'
export { useRuntimeHook } from './runtime-hook'

View File

@ -0,0 +1,21 @@
import { onScopeDispose } from 'vue'
import type { HookCallback } from 'hookable'
import { useNuxtApp } from '../nuxt'
import type { RuntimeNuxtHooks } from '../nuxt'
/**
* Registers a runtime hook in a Nuxt application and ensures it is properly disposed of when the scope is destroyed.
* @param name - The name of the hook to register.
* @param fn - The callback function to be executed when the hook is triggered.
* @since 3.14.0
*/
export function useRuntimeHook<THookName extends keyof RuntimeNuxtHooks> (
name: THookName,
fn: RuntimeNuxtHooks[THookName] extends HookCallback ? RuntimeNuxtHooks[THookName] : never,
): void {
const nuxtApp = useNuxtApp()
const unregister = nuxtApp.hook(name, fn)
onScopeDispose(unregister)
}

View File

@ -1,7 +1,7 @@
export { applyPlugin, applyPlugins, callWithNuxt, createNuxtApp, defineAppConfig, defineNuxtPlugin, definePayloadPlugin, isNuxtPlugin, registerPluginHooks, tryUseNuxtApp, useNuxtApp, useRuntimeConfig } from './nuxt'
export type { CreateOptions, NuxtApp, NuxtPayload, NuxtPluginIndicator, NuxtSSRContext, ObjectPlugin, Plugin, PluginEnvContext, PluginMeta, ResolvedPluginMeta, RuntimeNuxtHooks } from './nuxt'
export { defineNuxtComponent, useAsyncData, useLazyAsyncData, useNuxtData, refreshNuxtData, clearNuxtData, useHydration, callOnce, useState, clearNuxtState, clearError, createError, isNuxtError, showError, useError, useFetch, useLazyFetch, useCookie, refreshCookie, onPrehydrate, prerenderRoutes, useRequestHeaders, useRequestEvent, useRequestFetch, setResponseStatus, useResponseHeader, onNuxtReady, abortNavigation, addRouteMiddleware, defineNuxtRouteMiddleware, onBeforeRouteLeave, onBeforeRouteUpdate, setPageLayout, navigateTo, useRoute, useRouter, preloadComponents, prefetchComponents, preloadRouteComponents, isPrerendered, loadPayload, preloadPayload, definePayloadReducer, definePayloadReviver, getAppManifest, getRouteRules, reloadNuxtApp, useRequestURL, usePreviewMode, useId, useRouteAnnouncer, useHead, useSeoMeta, useServerSeoMeta } from './composables/index'
export { defineNuxtComponent, useAsyncData, useLazyAsyncData, useNuxtData, refreshNuxtData, clearNuxtData, useHydration, callOnce, useState, clearNuxtState, clearError, createError, isNuxtError, showError, useError, useFetch, useLazyFetch, useCookie, refreshCookie, onPrehydrate, prerenderRoutes, useRequestHeaders, useRequestEvent, useRequestFetch, setResponseStatus, useResponseHeader, onNuxtReady, abortNavigation, addRouteMiddleware, defineNuxtRouteMiddleware, onBeforeRouteLeave, onBeforeRouteUpdate, setPageLayout, navigateTo, useRoute, useRouter, preloadComponents, prefetchComponents, preloadRouteComponents, isPrerendered, loadPayload, preloadPayload, definePayloadReducer, definePayloadReviver, getAppManifest, getRouteRules, reloadNuxtApp, useRequestURL, usePreviewMode, useId, useRouteAnnouncer, useHead, useSeoMeta, useServerSeoMeta, useRuntimeHook } from './composables/index'
export type { AddRouteMiddlewareOptions, AsyncData, AsyncDataOptions, AsyncDataRequestStatus, CookieOptions, CookieRef, FetchResult, NuxtAppManifest, NuxtAppManifestMeta, NuxtError, ReloadNuxtAppOptions, RouteMiddleware, UseFetchOptions } from './composables/index'
export { defineNuxtLink } from './components/index'

View File

@ -47,7 +47,7 @@ export interface RuntimeNuxtHooks {
'app:chunkError': (options: { error: any }) => HookResult
'app:data:refresh': (keys?: string[]) => HookResult
'app:manifest:update': (meta?: NuxtAppManifestMeta) => HookResult
'dev:ssr-logs': (logs: LogObject[]) => void | Promise<void>
'dev:ssr-logs': (logs: LogObject[]) => HookResult
'link:prefetch': (link: string) => HookResult
'page:start': (Component?: VNode) => HookResult
'page:finish': (Component?: VNode) => HookResult

View File

@ -109,6 +109,10 @@ const granularAppPresets: InlinePreset[] = [
imports: ['useRouteAnnouncer'],
from: '#app/composables/route-announcer',
},
{
imports: ['useRuntimeHook'],
from: '#app/composables/runtime-hook',
},
]
export const scriptsStubsPreset = {

View File

@ -20,6 +20,7 @@ import { callOnce } from '#app/composables/once'
import { useLoadingIndicator } from '#app/composables/loading-indicator'
import { useRouteAnnouncer } from '#app/composables/route-announcer'
import { encodeURL, resolveRouteObject } from '#app/composables/router'
import { useRuntimeHook } from '#app/composables/runtime-hook'
import { asyncDataDefaults, nuxtDefaultErrorValue } from '#build/nuxt.config.mjs'
@ -95,6 +96,7 @@ describe('composables', () => {
'abortNavigation',
'setPageLayout',
'defineNuxtComponent',
'useRuntimeHook',
]
const skippedComposables: string[] = [
'addRouteMiddleware',
@ -580,6 +582,36 @@ describe.skipIf(process.env.TEST_MANIFEST === 'manifest-off')('app manifests', (
})
})
describe('useRuntimeHook', () => {
it('types work', () => {
// @ts-expect-error should not allow unknown hooks
useRuntimeHook('test', () => {})
useRuntimeHook('app:beforeMount', (_app) => {
// @ts-expect-error argument should be typed
_app = 'test'
})
})
it('should call hooks', async () => {
const nuxtApp = useNuxtApp()
let called = 1
const wrapper = await mountSuspended(defineNuxtComponent({
setup () {
useRuntimeHook('test-hook' as any, () => {
called++
})
},
render: () => h('div', 'hi there'),
}))
expect(called).toBe(1)
await nuxtApp.callHook('test-hook' as any)
expect(called).toBe(2)
wrapper.unmount()
await nuxtApp.callHook('test-hook' as any)
expect(called).toBe(2)
})
})
describe('routing utilities: `navigateTo`', () => {
it('navigateTo should disallow navigation to external URLs by default', () => {
expect(() => navigateTo('https://test.com')).toThrowErrorMatchingInlineSnapshot('[Error: Navigating to an external URL is not allowed by default. Use `navigateTo(url, { external: true })`.]')