mirror of
https://github.com/nuxt/nuxt.git
synced 2025-01-18 17:35:57 +00:00
feat(nuxt): add mode: 'navigation'
to callOnce
(#30260)
This commit is contained in:
parent
c83507e315
commit
f877823a43
@ -1,18 +1,23 @@
|
|||||||
import { useNuxtApp } from '../nuxt'
|
import { useNuxtApp } from '../nuxt'
|
||||||
|
|
||||||
|
type CallOnceOptions = {
|
||||||
|
mode?: 'navigation' | 'render'
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An SSR-friendly utility to call a method once
|
* An SSR-friendly utility to call a method once
|
||||||
* @param key a unique key ensuring the function can be properly de-duplicated across requests
|
* @param key a unique key ensuring the function can be properly de-duplicated across requests
|
||||||
* @param fn a function to call
|
* @param fn a function to call
|
||||||
|
* @param options Setup the mode, e.g. to re-execute on navigation
|
||||||
* @see https://nuxt.com/docs/api/utils/call-once
|
* @see https://nuxt.com/docs/api/utils/call-once
|
||||||
* @since 3.9.0
|
* @since 3.9.0
|
||||||
*/
|
*/
|
||||||
export function callOnce (key?: string, fn?: (() => any | Promise<any>)): Promise<void>
|
export function callOnce (key?: string, fn?: (() => any | Promise<any>), options?: CallOnceOptions): Promise<void>
|
||||||
export function callOnce (fn?: (() => any | Promise<any>)): Promise<void>
|
export function callOnce (fn?: (() => any | Promise<any>), options?: CallOnceOptions): Promise<void>
|
||||||
export async function callOnce (...args: any): Promise<void> {
|
export async function callOnce (...args: any): Promise<void> {
|
||||||
const autoKey = typeof args[args.length - 1] === 'string' ? args.pop() : undefined
|
const autoKey = typeof args[args.length - 1] === 'string' ? args.pop() : undefined
|
||||||
if (typeof args[0] !== 'string') { args.unshift(autoKey) }
|
if (typeof args[0] !== 'string') { args.unshift(autoKey) }
|
||||||
const [_key, fn] = args as [string, (() => any | Promise<any>)]
|
const [_key, fn, options] = args as [string, (() => any | Promise<any>), CallOnceOptions | undefined]
|
||||||
if (!_key || typeof _key !== 'string') {
|
if (!_key || typeof _key !== 'string') {
|
||||||
throw new TypeError('[nuxt] [callOnce] key must be a string: ' + _key)
|
throw new TypeError('[nuxt] [callOnce] key must be a string: ' + _key)
|
||||||
}
|
}
|
||||||
@ -20,10 +25,18 @@ export async function callOnce (...args: any): Promise<void> {
|
|||||||
throw new Error('[nuxt] [callOnce] fn must be a function: ' + fn)
|
throw new Error('[nuxt] [callOnce] fn must be a function: ' + fn)
|
||||||
}
|
}
|
||||||
const nuxtApp = useNuxtApp()
|
const nuxtApp = useNuxtApp()
|
||||||
|
|
||||||
|
if (options?.mode === 'navigation') {
|
||||||
|
nuxtApp.hooks.hookOnce('page:start', () => {
|
||||||
|
nuxtApp.payload.once.delete(_key)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// If key already ran
|
// If key already ran
|
||||||
if (nuxtApp.payload.once.has(_key)) {
|
if (nuxtApp.payload.once.has(_key)) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
nuxtApp._once = nuxtApp._once || {}
|
nuxtApp._once = nuxtApp._once || {}
|
||||||
nuxtApp._once[_key] = nuxtApp._once[_key] || fn() || true
|
nuxtApp._once[_key] = nuxtApp._once[_key] || fn() || true
|
||||||
await nuxtApp._once[_key]
|
await nuxtApp._once[_key]
|
||||||
|
@ -147,7 +147,7 @@ export default defineUntypedSchema({
|
|||||||
keyedComposables: {
|
keyedComposables: {
|
||||||
$resolve: (val: Array<{ name: string, argumentLength: string }> | undefined) => [
|
$resolve: (val: Array<{ name: string, argumentLength: string }> | undefined) => [
|
||||||
{ name: 'useId', argumentLength: 1 },
|
{ name: 'useId', argumentLength: 1 },
|
||||||
{ name: 'callOnce', argumentLength: 2 },
|
{ name: 'callOnce', argumentLength: 3 },
|
||||||
{ name: 'defineNuxtComponent', argumentLength: 2 },
|
{ name: 'defineNuxtComponent', argumentLength: 2 },
|
||||||
{ name: 'useState', argumentLength: 2 },
|
{ name: 'useState', argumentLength: 2 },
|
||||||
{ name: 'useFetch', argumentLength: 3 },
|
{ name: 'useFetch', argumentLength: 3 },
|
||||||
|
@ -1223,6 +1223,15 @@ describe('composables', () => {
|
|||||||
const { page } = await renderPage('/once')
|
const { page } = await renderPage('/once')
|
||||||
expect(await page.getByText('once:').textContent()).toContain('once: 2')
|
expect(await page.getByText('once:').textContent()).toContain('once: 2')
|
||||||
})
|
})
|
||||||
|
it('`callOnce` should run code once with navigation mode during initial render', async () => {
|
||||||
|
const html = await $fetch<string>('/once-nav-initial')
|
||||||
|
|
||||||
|
expect(html).toContain('once.vue')
|
||||||
|
expect(html).toContain('once: 2')
|
||||||
|
|
||||||
|
const { page } = await renderPage('/once-nav-initial')
|
||||||
|
expect(await page.getByText('once:').textContent()).toContain('once: 2')
|
||||||
|
})
|
||||||
it('`useId` should generate unique ids', async () => {
|
it('`useId` should generate unique ids', async () => {
|
||||||
// TODO: work around interesting Vue bug where async components are loaded in a different order on first import
|
// TODO: work around interesting Vue bug where async components are loaded in a different order on first import
|
||||||
await $fetch<string>('/use-id')
|
await $fetch<string>('/use-id')
|
||||||
|
14
test/fixtures/basic/pages/once-nav-initial.vue
vendored
Normal file
14
test/fixtures/basic/pages/once-nav-initial.vue
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<script setup>
|
||||||
|
const counter = useState('once', () => 0)
|
||||||
|
|
||||||
|
await callOnce(() => counter.value++, { mode: 'navigation' })
|
||||||
|
await callOnce('same-key', () => counter.value++, { mode: 'navigation' })
|
||||||
|
await callOnce('same-key', () => counter.value++, { mode: 'navigation' })
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div>once.vue</div>
|
||||||
|
<div>once: {{ counter }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
@ -1,6 +1,6 @@
|
|||||||
/// <reference path="../fixtures/basic/.nuxt/nuxt.d.ts" />
|
/// <reference path="../fixtures/basic/.nuxt/nuxt.d.ts" />
|
||||||
|
|
||||||
import { describe, expect, it, vi } from 'vitest'
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||||
import { defineEventHandler } from 'h3'
|
import { defineEventHandler } from 'h3'
|
||||||
import { destr } from 'destr'
|
import { destr } from 'destr'
|
||||||
|
|
||||||
@ -778,9 +778,18 @@ describe('useCookie', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
describe('callOnce', () => {
|
describe('callOnce', () => {
|
||||||
|
describe.each([
|
||||||
|
['without options', undefined],
|
||||||
|
['with "render" option', { mode: 'render' as const }],
|
||||||
|
['with "navigation" option', { mode: 'navigation' as const }],
|
||||||
|
])('%s', (_name, options) => {
|
||||||
|
const nuxtApp = useNuxtApp()
|
||||||
|
afterEach(() => {
|
||||||
|
nuxtApp.payload.once.clear()
|
||||||
|
})
|
||||||
it('should only call composable once', async () => {
|
it('should only call composable once', async () => {
|
||||||
const fn = vi.fn()
|
const fn = vi.fn()
|
||||||
const execute = () => callOnce(fn)
|
const execute = () => options ? callOnce(fn, options) : callOnce(fn)
|
||||||
await execute()
|
await execute()
|
||||||
await execute()
|
await execute()
|
||||||
expect(fn).toHaveBeenCalledTimes(1)
|
expect(fn).toHaveBeenCalledTimes(1)
|
||||||
@ -788,24 +797,37 @@ describe('callOnce', () => {
|
|||||||
|
|
||||||
it('should only call composable once when called in parallel', async () => {
|
it('should only call composable once when called in parallel', async () => {
|
||||||
const fn = vi.fn().mockImplementation(() => new Promise(resolve => setTimeout(resolve, 1)))
|
const fn = vi.fn().mockImplementation(() => new Promise(resolve => setTimeout(resolve, 1)))
|
||||||
const execute = () => callOnce(fn)
|
const execute = () => options ? callOnce(fn, options) : callOnce(fn)
|
||||||
await Promise.all([execute(), execute(), execute()])
|
await Promise.all([execute(), execute(), execute()])
|
||||||
expect(fn).toHaveBeenCalledTimes(1)
|
expect(fn).toHaveBeenCalledTimes(1)
|
||||||
|
|
||||||
const fnSync = vi.fn().mockImplementation(() => {})
|
const fnSync = vi.fn().mockImplementation(() => {})
|
||||||
const executeSync = () => callOnce(fnSync)
|
const executeSync = () => options ? callOnce(fnSync, options) : callOnce(fnSync)
|
||||||
await Promise.all([executeSync(), executeSync(), executeSync()])
|
await Promise.all([executeSync(), executeSync(), executeSync()])
|
||||||
expect(fnSync).toHaveBeenCalledTimes(1)
|
expect(fnSync).toHaveBeenCalledTimes(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should use key to dedupe', async () => {
|
it('should use key to dedupe', async () => {
|
||||||
const fn = vi.fn()
|
const fn = vi.fn()
|
||||||
const execute = (key?: string) => callOnce(key, fn)
|
const execute = (key?: string) => options ? callOnce(key, fn, options) : callOnce(key, fn)
|
||||||
await execute('first')
|
await execute('first')
|
||||||
await execute('first')
|
await execute('first')
|
||||||
await execute('second')
|
await execute('second')
|
||||||
expect(fn).toHaveBeenCalledTimes(2)
|
expect(fn).toHaveBeenCalledTimes(2)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it.runIf(options?.mode === 'navigation')('should rerun on navigation', async () => {
|
||||||
|
const fn = vi.fn()
|
||||||
|
const execute = () => options ? callOnce(fn, options) : callOnce(fn)
|
||||||
|
await execute()
|
||||||
|
await execute()
|
||||||
|
expect(fn).toHaveBeenCalledTimes(1)
|
||||||
|
|
||||||
|
await nuxtApp.callHook('page:start')
|
||||||
|
await execute()
|
||||||
|
expect(fn).toHaveBeenCalledTimes(2)
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('route announcer', () => {
|
describe('route announcer', () => {
|
||||||
|
Loading…
Reference in New Issue
Block a user