mirror of
https://github.com/nuxt/nuxt.git
synced 2025-01-18 09:25:54 +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'
|
||||
|
||||
type CallOnceOptions = {
|
||||
mode?: 'navigation' | 'render'
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 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
|
||||
* @since 3.9.0
|
||||
*/
|
||||
export function callOnce (key?: string, fn?: (() => any | Promise<any>)): Promise<void>
|
||||
export function callOnce (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>), options?: CallOnceOptions): Promise<void>
|
||||
export async function callOnce (...args: any): Promise<void> {
|
||||
const autoKey = typeof args[args.length - 1] === 'string' ? args.pop() : undefined
|
||||
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') {
|
||||
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)
|
||||
}
|
||||
const nuxtApp = useNuxtApp()
|
||||
|
||||
if (options?.mode === 'navigation') {
|
||||
nuxtApp.hooks.hookOnce('page:start', () => {
|
||||
nuxtApp.payload.once.delete(_key)
|
||||
})
|
||||
}
|
||||
|
||||
// If key already ran
|
||||
if (nuxtApp.payload.once.has(_key)) {
|
||||
return
|
||||
}
|
||||
|
||||
nuxtApp._once = nuxtApp._once || {}
|
||||
nuxtApp._once[_key] = nuxtApp._once[_key] || fn() || true
|
||||
await nuxtApp._once[_key]
|
||||
|
@ -147,7 +147,7 @@ export default defineUntypedSchema({
|
||||
keyedComposables: {
|
||||
$resolve: (val: Array<{ name: string, argumentLength: string }> | undefined) => [
|
||||
{ name: 'useId', argumentLength: 1 },
|
||||
{ name: 'callOnce', argumentLength: 2 },
|
||||
{ name: 'callOnce', argumentLength: 3 },
|
||||
{ name: 'defineNuxtComponent', argumentLength: 2 },
|
||||
{ name: 'useState', argumentLength: 2 },
|
||||
{ name: 'useFetch', argumentLength: 3 },
|
||||
|
@ -1223,6 +1223,15 @@ describe('composables', () => {
|
||||
const { page } = await renderPage('/once')
|
||||
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 () => {
|
||||
// TODO: work around interesting Vue bug where async components are loaded in a different order on first import
|
||||
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" />
|
||||
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest'
|
||||
import { defineEventHandler } from 'h3'
|
||||
import { destr } from 'destr'
|
||||
|
||||
@ -778,33 +778,55 @@ describe('useCookie', () => {
|
||||
})
|
||||
|
||||
describe('callOnce', () => {
|
||||
it('should only call composable once', async () => {
|
||||
const fn = vi.fn()
|
||||
const execute = () => callOnce(fn)
|
||||
await execute()
|
||||
await execute()
|
||||
expect(fn).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
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 () => {
|
||||
const fn = vi.fn()
|
||||
const execute = () => options ? callOnce(fn, options) : callOnce(fn)
|
||||
await execute()
|
||||
await execute()
|
||||
expect(fn).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should only call composable once when called in parallel', async () => {
|
||||
const fn = vi.fn().mockImplementation(() => new Promise(resolve => setTimeout(resolve, 1)))
|
||||
const execute = () => callOnce(fn)
|
||||
await Promise.all([execute(), execute(), execute()])
|
||||
expect(fn).toHaveBeenCalledTimes(1)
|
||||
it('should only call composable once when called in parallel', async () => {
|
||||
const fn = vi.fn().mockImplementation(() => new Promise(resolve => setTimeout(resolve, 1)))
|
||||
const execute = () => options ? callOnce(fn, options) : callOnce(fn)
|
||||
await Promise.all([execute(), execute(), execute()])
|
||||
expect(fn).toHaveBeenCalledTimes(1)
|
||||
|
||||
const fnSync = vi.fn().mockImplementation(() => {})
|
||||
const executeSync = () => callOnce(fnSync)
|
||||
await Promise.all([executeSync(), executeSync(), executeSync()])
|
||||
expect(fnSync).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
const fnSync = vi.fn().mockImplementation(() => {})
|
||||
const executeSync = () => options ? callOnce(fnSync, options) : callOnce(fnSync)
|
||||
await Promise.all([executeSync(), executeSync(), executeSync()])
|
||||
expect(fnSync).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should use key to dedupe', async () => {
|
||||
const fn = vi.fn()
|
||||
const execute = (key?: string) => callOnce(key, fn)
|
||||
await execute('first')
|
||||
await execute('first')
|
||||
await execute('second')
|
||||
expect(fn).toHaveBeenCalledTimes(2)
|
||||
it('should use key to dedupe', async () => {
|
||||
const fn = vi.fn()
|
||||
const execute = (key?: string) => options ? callOnce(key, fn, options) : callOnce(key, fn)
|
||||
await execute('first')
|
||||
await execute('first')
|
||||
await execute('second')
|
||||
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)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user