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

This commit is contained in:
Damian Głowala 2024-11-02 23:25:05 +01:00 committed by GitHub
parent edef832700
commit 2aa4daab92
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
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 { usePreviewMode } from './preview'
export { useId } from './id' export { useId } from './id'
export { useRouteAnnouncer } from './route-announcer' 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 { 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 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 type { AddRouteMiddlewareOptions, AsyncData, AsyncDataOptions, AsyncDataRequestStatus, CookieOptions, CookieRef, FetchResult, NuxtAppManifest, NuxtAppManifestMeta, NuxtError, ReloadNuxtAppOptions, RouteMiddleware, UseFetchOptions } from './composables/index'
export { defineNuxtLink } from './components/index' export { defineNuxtLink } from './components/index'

View File

@ -45,7 +45,7 @@ export interface RuntimeNuxtHooks {
'app:chunkError': (options: { error: any }) => HookResult 'app:chunkError': (options: { error: any }) => HookResult
'app:data:refresh': (keys?: string[]) => HookResult 'app:data:refresh': (keys?: string[]) => HookResult
'app:manifest:update': (meta?: NuxtAppManifestMeta) => 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 'link:prefetch': (link: string) => HookResult
'page:start': (Component?: VNode) => HookResult 'page:start': (Component?: VNode) => HookResult
'page:finish': (Component?: VNode) => HookResult 'page:finish': (Component?: VNode) => HookResult

View File

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

View File

@ -20,6 +20,7 @@ import { callOnce } from '#app/composables/once'
import { useLoadingIndicator } from '#app/composables/loading-indicator' import { useLoadingIndicator } from '#app/composables/loading-indicator'
import { useRouteAnnouncer } from '#app/composables/route-announcer' import { useRouteAnnouncer } from '#app/composables/route-announcer'
import { encodeURL, resolveRouteObject } from '#app/composables/router' import { encodeURL, resolveRouteObject } from '#app/composables/router'
import { useRuntimeHook } from '#app/composables/runtime-hook'
registerEndpoint('/api/test', defineEventHandler(event => ({ registerEndpoint('/api/test', defineEventHandler(event => ({
method: event.method, method: event.method,
@ -93,6 +94,7 @@ describe('composables', () => {
'abortNavigation', 'abortNavigation',
'setPageLayout', 'setPageLayout',
'defineNuxtComponent', 'defineNuxtComponent',
'useRuntimeHook',
] ]
const skippedComposables: string[] = [ const skippedComposables: string[] = [
'addRouteMiddleware', 'addRouteMiddleware',
@ -577,6 +579,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`', () => { describe('routing utilities: `navigateTo`', () => {
it('navigateTo should disallow navigation to external URLs by default', () => { 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 })`.]') expect(() => navigateTo('https://test.com')).toThrowErrorMatchingInlineSnapshot('[Error: Navigating to an external URL is not allowed by default. Use `navigateTo(url, { external: true })`.]')