import { getCurrentInstance, onBeforeMount, onUnmounted, Ref, ref, unref, UnwrapRef, watch } from 'vue' import { Nuxt, useNuxt } from '@nuxt/app' import { NuxtComponentPendingPromises } from './component' import { ensureReactive, useGlobalData } from './data' export type AsyncDataFn = (ctx?: Nuxt) => Promise export interface AsyncDataOptions { server?: boolean defer?: boolean } export interface AsyncDataState { data: UnwrapRef pending: Ref fetch: (force?: boolean) => Promise> error?: any } export type AsyncDataResult = AsyncDataState & Promise> export function useAsyncData (defaults?: AsyncDataOptions) { const nuxt = useNuxt() const vm = getCurrentInstance() const onBeforeMountCbs: Array<() => void> = [] if (process.client) { onBeforeMount(() => { onBeforeMountCbs.forEach((cb) => { cb() }) onBeforeMountCbs.splice(0, onBeforeMountCbs.length) }) onUnmounted(() => onBeforeMountCbs.splice(0, onBeforeMountCbs.length)) } nuxt._asyncDataPromises = nuxt._asyncDataPromises || {} return function asyncData> ( key: string, handler: AsyncDataFn, options: AsyncDataOptions = {} ): AsyncDataResult { if (typeof handler !== 'function') { throw new TypeError('asyncData handler must be a function') } options = { server: true, defer: false, ...defaults, ...options } const globalData = useGlobalData(nuxt) const state = { data: ensureReactive(globalData, key) as UnwrapRef, pending: ref(true) } as AsyncDataState const fetch = (force?: boolean): Promise> => { if (nuxt._asyncDataPromises[key] && !force) { return nuxt._asyncDataPromises[key] } state.pending.value = true nuxt._asyncDataPromises[key] = Promise.resolve(handler(nuxt)).then((result) => { for (const _key in result) { state.data[_key] = unref(result[_key]) } return state.data }).finally(() => { state.pending.value = false nuxt._asyncDataPromises[key] = null }) return nuxt._asyncDataPromises[key] } const fetchOnServer = options.server !== false const clientOnly = options.server === false // Server side if (process.server && fetchOnServer) { fetch() } // Client side if (process.client) { // Watch handler watch(handler.bind(null, nuxt), fetch) // 1. Hydration (server: true): no fetch if (nuxt.isHydrating && fetchOnServer) { state.pending.value = false } // 2. Initial load (server: false): fetch on mounted if (nuxt.isHydrating && clientOnly) { // Fetch on mounted (initial load or deferred fetch) onBeforeMountCbs.push(fetch) } else if (!nuxt.isHydrating) { // Navigation if (options.defer) { // 3. Navigation (defer: true): fetch on mounted onBeforeMountCbs.push(fetch) } else { // 4. Navigation (defer: false): await fetch fetch() } } } // Auto enqueue if within nuxt component instance if (nuxt._asyncDataPromises[key] && vm[NuxtComponentPendingPromises]) { vm[NuxtComponentPendingPromises].push(nuxt._asyncDataPromises[key]) } const res = Promise.resolve(nuxt._asyncDataPromises[key]).then(() => state) as AsyncDataResult res.data = state.data res.pending = state.pending res.fetch = fetch return res } } export function asyncData> ( key: string, handler: AsyncDataFn, options?: AsyncDataOptions ): AsyncDataResult { return useAsyncData()(key, handler, options) }