Skip to content

Commit

Permalink
refactor(nuxt3): cleanup data fetching and improved useAsyncData (n…
Browse files Browse the repository at this point in the history
  • Loading branch information
pi0 authored Oct 8, 2021
1 parent e614328 commit 2bf645b
Show file tree
Hide file tree
Showing 15 changed files with 106 additions and 185 deletions.
2 changes: 1 addition & 1 deletion docs/content/2.app/3.data-fetching.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Under the hood, `defer: false` uses `<Suspense>` to block the loading of the rou

```vue
<script setup>
const { data } = await asyncData('time', () => $fetch('/api/count'))
const { data } = await useAsyncData('time', () => $fetch('/api/count'))
</script>
<template>
Expand Down
7 changes: 5 additions & 2 deletions examples/async-data/pages/index.vue
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
<template>
<div>
Page visits: {{ data.count }}
{{ data }}
<button :disabled="pending" @click="refresh">
Refrash Data
</button>
</div>
</template>

<script setup>
const { data } = await asyncData('time', () => $fetch('/api/count'))
const { data, refresh, pending } = await useAsyncData('/api/hello', () => $fetch('/api/hello'))
</script>
3 changes: 0 additions & 3 deletions examples/async-data/server/api/count.js

This file was deleted.

1 change: 1 addition & 0 deletions examples/async-data/server/api/hello.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default () => `Hello world! (Generated at ${new Date().toGMTString()})`
2 changes: 1 addition & 1 deletion examples/with-vue-content-loader/pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ import { ContentLoader } from 'vue-content-loader'
export default defineNuxtComponent({
components: { ContentLoader },
setup () {
const { data, pending } = asyncData(
const { data, pending } = useAsyncData(
'time',
() =>
new Promise(resolve =>
Expand Down
4 changes: 0 additions & 4 deletions packages/bridge/src/runtime/composables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,6 @@ export * from '@vue/composition-api'
const mock = () => () => { throw new Error('not implemented') }

export const useAsyncData = mock()
export const asyncData = mock()
export const useSSRRef = mock()
export const useData = mock()
export const useGlobalData = mock()
export const useHydration = mock()

// Runtime config helper
Expand Down
2 changes: 1 addition & 1 deletion packages/kit/src/config/schema/_app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -317,7 +317,7 @@ export default {
/** Set to false to disable the Nuxt `validate()` hook */
validate: true,
/** Set to false to disable the Nuxt `asyncData()` hook */
asyncData: true,
useAsyncData: true,
/** Set to false to disable the Nuxt `fetch()` hook */
fetch: true,
/** Set to false to disable `$nuxt.isOnline` */
Expand Down
2 changes: 1 addition & 1 deletion packages/kit/src/config/schema/generate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export default {
* @example
* ```js
* export default {
* async asyncData ({ params, error, payload }) {
* async useAsyncData ({ params, error, payload }) {
* if (payload) return { user: payload }
* else return { user: await backend.fetchUser(params.id) }
* }
Expand Down
178 changes: 87 additions & 91 deletions packages/nuxt3/src/app/composables/asyncData.ts
Original file line number Diff line number Diff line change
@@ -1,119 +1,115 @@
import { onBeforeMount, onUnmounted, ref, unref } from 'vue'
import type { UnwrapRef, Ref } from 'vue'

import { ensureReactive, useGlobalData } from './data'
import { onBeforeMount, onUnmounted, ref } from 'vue'
import type { Ref } from 'vue'
import { NuxtApp, useNuxtApp } from '#app'

export type AsyncDataFn<T> = (ctx?: NuxtApp) => Promise<T>

export interface AsyncDataOptions {
export interface AsyncDataOptions<T> {
server?: boolean
defer?: boolean
default?: () => T
}

export interface AsyncDataState<T> {
data: UnwrapRef<T>
export interface _AsyncData<T> {
data: Ref<T>
pending: Ref<boolean>
fetch: (force?: boolean) => Promise<UnwrapRef<T>>
refresh: (force?: boolean) => Promise<void>
error?: any
}

export type AsyncDataResult<T> = AsyncDataState<T> & Promise<AsyncDataState<T>>
export type AsyncData<T> = _AsyncData<T> & Promise<_AsyncData<T>>

export function useAsyncData (defaults?: AsyncDataOptions) {
const nuxt = useNuxtApp()
const onBeforeMountCbs: Array<() => void> = []
const getDefault = () => null

if (process.client) {
onBeforeMount(() => {
onBeforeMountCbs.forEach((cb) => { cb() })
onBeforeMountCbs.splice(0, onBeforeMountCbs.length)
})

onUnmounted(() => onBeforeMountCbs.splice(0, onBeforeMountCbs.length))
export function useAsyncData<T extends Record<string, any>> (key: string, handler: (ctx?: NuxtApp) => Promise<T>, options: AsyncDataOptions<T> = {}): AsyncData<T> {
// Validate arguments
if (typeof key !== 'string') {
throw new TypeError('asyncData key must be a string')
}
if (typeof handler !== 'function') {
throw new TypeError('asyncData handler must be a function')
}

nuxt._asyncDataPromises = nuxt._asyncDataPromises || {}
// Apply defaults
options = { server: true, defer: false, default: getDefault, ...options }

return function asyncData<T extends Record<string, any>> (
key: string,
handler: AsyncDataFn<T>,
options: AsyncDataOptions = {}
): AsyncDataResult<T> {
if (typeof handler !== 'function') {
throw new TypeError('asyncData handler must be a function')
}
options = {
server: true,
defer: false,
...defaults,
...options
}
// Setup nuxt instance payload
const nuxt = useNuxtApp()

const globalData = useGlobalData(nuxt)
// Setup hook callbacks once per instance
const instance = getCurrentInstance()
if (!instance._nuxtOnBeforeMountCbs) {
const cbs = instance._nuxtOnBeforeMountCbs = []
if (instance && process.client) {
onBeforeMount(() => {
cbs.forEach((cb) => { cb() })
cbs.splice(0, cbs.length)
})
onUnmounted(() => cbs.splice(0, cbs.length))
}
}

const state = {
data: ensureReactive(globalData, key) as UnwrapRef<T>,
pending: ref(true)
} as AsyncDataState<T>
const asyncData = {
data: ref(nuxt.payload.data[key] ?? options.default()),
pending: ref(true),
error: ref(null)
} as AsyncData<T>

const fetch = (force?: boolean): Promise<UnwrapRef<T>> => {
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) {
// @ts-expect-error
state.data[_key] = unref(result[_key])
}
return state.data
}).finally(() => {
state.pending.value = false
nuxt._asyncDataPromises[key] = null
})
asyncData.refresh = (force?: boolean) => {
// Avoid fetching same key more than once at a time
if (nuxt._asyncDataPromises[key] && !force) {
return nuxt._asyncDataPromises[key]
}
asyncData.pending.value = true
// TODO: Cancel previus promise
// TODO: Handle immediate errors
nuxt._asyncDataPromises[key] = Promise.resolve(handler(nuxt))
.then((result) => {
asyncData.data.value = result
asyncData.error.value = null
})
.catch((error: any) => {
asyncData.error.value = error
asyncData.data.value = options.default()
})
.finally(() => {
asyncData.pending.value = false
nuxt.payload.data[key] = asyncData.data.value
delete nuxt._asyncDataPromises[key]
})
return nuxt._asyncDataPromises[key]
}

const fetchOnServer = options.server !== false
const clientOnly = options.server === false
const fetchOnServer = options.server !== false
const clientOnly = options.server === false

// Server side
if (process.server && fetchOnServer) {
fetch()
}
// Server side
if (process.server && fetchOnServer) {
asyncData.refresh()
}

// Client side
if (process.client) {
// 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()
}
// Client side
if (process.client) {
// 1. Hydration (server: true): no fetch
if (nuxt.isHydrating && fetchOnServer) {
asyncData.pending.value = false
}
// 2. Initial load (server: false): fetch on mounted
if (nuxt.isHydrating && clientOnly) {
// Fetch on mounted (initial load or deferred fetch)
instance._nuxtOnBeforeMountCbs.push(asyncData.refresh)
} else if (!nuxt.isHydrating) { // Navigation
if (options.defer) {
// 3. Navigation (defer: true): fetch on mounted
instance._nuxtOnBeforeMountCbs.push(asyncData.refresh)
} else {
// 4. Navigation (defer: false): await fetch
asyncData.refresh()
}
}

const res = Promise.resolve(nuxt._asyncDataPromises[key]).then(() => state) as AsyncDataResult<T>
res.data = state.data
res.pending = state.pending
res.fetch = fetch
return res
}
}

export function asyncData<T extends Record<string, any>> (
key: string, handler: AsyncDataFn<T>, options?: AsyncDataOptions
): AsyncDataResult<T> {
return useAsyncData()(key, handler, options)
// Allow directly awaiting on asyncData
const asyncDataPromise = Promise.resolve(nuxt._asyncDataPromises[key]).then(() => asyncData) as AsyncData<T>
Object.assign(asyncDataPromise, asyncData)

return asyncDataPromise as AsyncData<T>
}
4 changes: 2 additions & 2 deletions packages/nuxt3/src/app/composables/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { DefineComponent } from 'vue'
import { useRoute } from 'vue-router'
import type { LegacyContext } from '../legacy'
import { useNuxtApp } from '../nuxt'
import { asyncData } from './asyncData'
import { useAsyncData } from './asyncData'

export const NuxtComponentIndicator = '__nuxt_component'

Expand All @@ -14,7 +14,7 @@ async function runLegacyAsyncData (res: Record<string, any> | Promise<Record<str
const vm = getCurrentInstance()
const { fetchKey } = vm.proxy.$options
const key = typeof fetchKey === 'function' ? fetchKey(() => '') : fetchKey || route.fullPath
const { data } = await asyncData(`options:asyncdata:${key}`, () => fn(nuxt._legacyContext))
const { data } = await useAsyncData(`options:asyncdata:${key}`, () => fn(nuxt._legacyContext))
Object.assign(await res, toRefs(data))
}

Expand Down
65 changes: 0 additions & 65 deletions packages/nuxt3/src/app/composables/data.ts

This file was deleted.

3 changes: 1 addition & 2 deletions packages/nuxt3/src/app/composables/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export { defineNuxtComponent } from './component'
export { useAsyncData, asyncData } from './asyncData'
export { useData } from './data'
export { useAsyncData } from './asyncData'
export { useHydration } from './hydrate'
14 changes: 3 additions & 11 deletions packages/nuxt3/src/app/nuxt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,9 @@ export function createNuxtApp (options: CreateOptions) {
const nuxt: NuxtApp = {
provide: undefined,
globalName: 'nuxt',
payload: {},
payload: reactive(process.server ? { serverRendered: true, data: {} } : (window.__NUXT__ || { data: {} })),
isHydrating: process.client,
_asyncDataPromises: {},
...options
} as any as NuxtApp

Expand All @@ -95,20 +96,11 @@ export function createNuxtApp (options: CreateOptions) {
}

if (process.server) {
nuxt.payload = {
serverRendered: true
}

nuxt.ssrContext = nuxt.ssrContext || {}

// Expose to server renderer to create window.__NUXT__
nuxt.ssrContext = nuxt.ssrContext || {}
nuxt.ssrContext.payload = nuxt.payload
}

if (process.client) {
nuxt.payload = window.__NUXT__ || {}
}

// Expose runtime config
if (process.server) {
nuxt.provide('config', options.ssrContext.runtimeConfig.private)
Expand Down
Loading

0 comments on commit 2bf645b

Please sign in to comment.