mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-25 23:22:02 +00:00
feat(app): asyncData with global state and explicit key (#37)
Co-authored-by: Pooya Parsa <pyapar@gmail.com> Co-authored-by: Sébastien Chopin <seb@nuxtjs.com>
This commit is contained in:
parent
2849559598
commit
efabacd8e2
@ -1,7 +1,8 @@
|
|||||||
import { Ref, ref, onMounted, watch, getCurrentInstance, onUnmounted } from 'vue'
|
import { getCurrentInstance, onBeforeMount, onUnmounted, Ref, ref, unref, UnwrapRef, watch } from 'vue'
|
||||||
import { Nuxt, useNuxt } from '@nuxt/app'
|
import { Nuxt, useNuxt } from '@nuxt/app'
|
||||||
|
|
||||||
import { ensureReactive, useData } from './data'
|
import { NuxtComponentPendingPromises } from './component'
|
||||||
|
import { ensureReactive, useGlobalData } from './data'
|
||||||
|
|
||||||
export type AsyncDataFn<T> = (ctx?: Nuxt) => Promise<T>
|
export type AsyncDataFn<T> = (ctx?: Nuxt) => Promise<T>
|
||||||
|
|
||||||
@ -10,35 +11,36 @@ export interface AsyncDataOptions {
|
|||||||
defer?: boolean
|
defer?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AsyncDataObj<T> {
|
export interface AsyncDataState<T> {
|
||||||
data: Ref<T>
|
data: UnwrapRef<T>
|
||||||
pending: Ref<boolean>
|
pending: Ref<boolean>
|
||||||
refresh: () => Promise<void>
|
fetch: (force?: boolean) => Promise<UnwrapRef<T>>
|
||||||
error?: any
|
error?: any
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type AsyncDataResult<T> = AsyncDataState<T> & Promise<AsyncDataState<T>>
|
||||||
|
|
||||||
export function useAsyncData (defaults?: AsyncDataOptions) {
|
export function useAsyncData (defaults?: AsyncDataOptions) {
|
||||||
const nuxt = useNuxt()
|
const nuxt = useNuxt()
|
||||||
const vm = getCurrentInstance()
|
const vm = getCurrentInstance()
|
||||||
|
const onBeforeMountCbs: Array<() => void> = []
|
||||||
const data = useData(nuxt, vm)
|
|
||||||
let dataRef = 1
|
|
||||||
|
|
||||||
const onMountedCbs: Array<() => void> = []
|
|
||||||
|
|
||||||
if (process.client) {
|
if (process.client) {
|
||||||
onMounted(() => {
|
onBeforeMount(() => {
|
||||||
onMountedCbs.forEach((cb) => { cb() })
|
onBeforeMountCbs.forEach((cb) => { cb() })
|
||||||
onMountedCbs.splice(0, onMountedCbs.length)
|
onBeforeMountCbs.splice(0, onBeforeMountCbs.length)
|
||||||
})
|
})
|
||||||
|
|
||||||
onUnmounted(() => onMountedCbs.splice(0, onMountedCbs.length))
|
onUnmounted(() => onBeforeMountCbs.splice(0, onBeforeMountCbs.length))
|
||||||
}
|
}
|
||||||
|
|
||||||
return async function asyncData<T = Record<string, any>> (
|
nuxt._asyncDataPromises = nuxt._asyncDataPromises || {}
|
||||||
|
|
||||||
|
return function asyncData<T extends Record<string, any>> (
|
||||||
|
key: string,
|
||||||
handler: AsyncDataFn<T>,
|
handler: AsyncDataFn<T>,
|
||||||
options?: AsyncDataOptions
|
options: AsyncDataOptions = {}
|
||||||
): Promise<AsyncDataObj<T>> {
|
): AsyncDataResult<T> {
|
||||||
if (typeof handler !== 'function') {
|
if (typeof handler !== 'function') {
|
||||||
throw new TypeError('asyncData handler must be a function')
|
throw new TypeError('asyncData handler must be a function')
|
||||||
}
|
}
|
||||||
@ -49,71 +51,77 @@ export function useAsyncData (defaults?: AsyncDataOptions) {
|
|||||||
...options
|
...options
|
||||||
}
|
}
|
||||||
|
|
||||||
const key = String(dataRef++)
|
const globalData = useGlobalData(nuxt)
|
||||||
const pending = ref(true)
|
|
||||||
|
|
||||||
const datastore = ensureReactive(data, key)
|
const state = {
|
||||||
|
data: ensureReactive(globalData, key) as UnwrapRef<T>,
|
||||||
const fetch = async () => {
|
pending: ref(true)
|
||||||
pending.value = true
|
} as AsyncDataState<T>
|
||||||
const _handler = handler(nuxt)
|
|
||||||
|
|
||||||
if (_handler instanceof Promise) {
|
|
||||||
// Let user resolve if request is promise
|
|
||||||
// TODO: handle error
|
|
||||||
const result = await _handler
|
|
||||||
|
|
||||||
|
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) {
|
for (const _key in result) {
|
||||||
datastore[_key] = result[_key]
|
state.data[_key] = unref(result[_key])
|
||||||
}
|
}
|
||||||
|
return state.data
|
||||||
pending.value = false
|
}).finally(() => {
|
||||||
} else {
|
state.pending.value = false
|
||||||
// Invalid request
|
nuxt._asyncDataPromises[key] = null
|
||||||
throw new TypeError('Invalid asyncData handler: ' + _handler)
|
})
|
||||||
}
|
return nuxt._asyncDataPromises[key]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const fetchOnServer = options.server !== false
|
||||||
const clientOnly = options.server === false
|
const clientOnly = options.server === false
|
||||||
|
|
||||||
|
// Server side
|
||||||
|
if (process.server && fetchOnServer) {
|
||||||
|
fetch()
|
||||||
|
}
|
||||||
|
|
||||||
// Client side
|
// Client side
|
||||||
if (process.client) {
|
if (process.client) {
|
||||||
// 1. Hydration (server: true): no fetch
|
|
||||||
if (nuxt.isHydrating && options.server) {
|
|
||||||
pending.value = false
|
|
||||||
}
|
|
||||||
// 2. Initial load (server: false): fetch on mounted
|
|
||||||
if (nuxt.isHydrating && !options.server) {
|
|
||||||
// Fetch on mounted (initial load or deferred fetch)
|
|
||||||
onMountedCbs.push(fetch)
|
|
||||||
} else if (!nuxt.isHydrating) {
|
|
||||||
if (options.defer) {
|
|
||||||
// 3. Navigation (defer: true): fetch on mounted
|
|
||||||
onMountedCbs.push(fetch)
|
|
||||||
} else {
|
|
||||||
// 4. Navigation (defer: false): await fetch
|
|
||||||
await fetch()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Watch handler
|
// Watch handler
|
||||||
watch(handler.bind(null, nuxt), fetch)
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Server side
|
// Auto enqueue if within nuxt component instance
|
||||||
if (process.server && !clientOnly) {
|
if (nuxt._asyncDataPromises[key] && vm[NuxtComponentPendingPromises]) {
|
||||||
await fetch()
|
vm[NuxtComponentPendingPromises].push(nuxt._asyncDataPromises[key])
|
||||||
}
|
|
||||||
return {
|
|
||||||
data: datastore,
|
|
||||||
pending,
|
|
||||||
refresh: fetch
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 = Record<string, any>> (
|
export function asyncData<T extends Record<string, any>> (
|
||||||
handler: AsyncDataFn<T>,
|
key: string, handler: AsyncDataFn<T>, options?: AsyncDataOptions
|
||||||
options?: AsyncDataOptions
|
): AsyncDataResult<T> {
|
||||||
): Promise<AsyncDataObj<T>> {
|
return useAsyncData()(key, handler, options)
|
||||||
return useAsyncData()(handler, options)
|
|
||||||
}
|
}
|
||||||
|
59
packages/app/src/composables/component.ts
Normal file
59
packages/app/src/composables/component.ts
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
import { ComponentInternalInstance, DefineComponent, defineComponent, getCurrentInstance } from 'vue'
|
||||||
|
|
||||||
|
export const NuxtComponentIndicator = '__nuxt_component'
|
||||||
|
export const NuxtComponentPendingPromises = '_pendingPromises'
|
||||||
|
|
||||||
|
export interface NuxtComponentInternalInstance extends ComponentInternalInstance {
|
||||||
|
[NuxtComponentPendingPromises]: Array<Promise<void>>
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCurrentNuxtComponentInstance (): NuxtComponentInternalInstance {
|
||||||
|
const vm = getCurrentInstance() as NuxtComponentInternalInstance
|
||||||
|
|
||||||
|
if (!vm || !vm.proxy.$options[NuxtComponentIndicator]) {
|
||||||
|
throw new Error('This method can only be used within a component defined with `defineNuxtComponent()`.')
|
||||||
|
}
|
||||||
|
|
||||||
|
return vm
|
||||||
|
}
|
||||||
|
|
||||||
|
export function enqueueNuxtComponent (p: Promise<void>) {
|
||||||
|
const vm = getCurrentNuxtComponentInstance()
|
||||||
|
vm[NuxtComponentPendingPromises].push(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const defineNuxtComponent: typeof defineComponent =
|
||||||
|
function defineNuxtComponent (options: any): any {
|
||||||
|
const { setup } = options
|
||||||
|
|
||||||
|
if (!setup) {
|
||||||
|
return {
|
||||||
|
[NuxtComponentIndicator]: true,
|
||||||
|
...options
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
[NuxtComponentIndicator]: true,
|
||||||
|
...options,
|
||||||
|
setup (props, ctx) {
|
||||||
|
const vm = getCurrentNuxtComponentInstance()
|
||||||
|
let promises = vm[NuxtComponentPendingPromises] = vm[NuxtComponentPendingPromises] || []
|
||||||
|
|
||||||
|
const res = setup(props, ctx)
|
||||||
|
|
||||||
|
if (!promises.length && !(res instanceof Promise)) {
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.resolve(res)
|
||||||
|
.then(() => Promise.all(promises))
|
||||||
|
.then(() => res)
|
||||||
|
.finally(() => {
|
||||||
|
promises.length = 0
|
||||||
|
promises = null
|
||||||
|
delete vm[NuxtComponentPendingPromises]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} as DefineComponent
|
||||||
|
}
|
@ -1,5 +1,4 @@
|
|||||||
import { getCurrentInstance, isReactive, reactive, UnwrapRef } from 'vue'
|
import { getCurrentInstance, isReactive, reactive, UnwrapRef } from 'vue'
|
||||||
|
|
||||||
import { useNuxt } from '@nuxt/app'
|
import { useNuxt } from '@nuxt/app'
|
||||||
|
|
||||||
export function ensureReactive<
|
export function ensureReactive<
|
||||||
@ -51,3 +50,12 @@ export function useData<T = Record<string, any>> (
|
|||||||
|
|
||||||
return ensureReactive(nuxt.payload.data, ssrRef)
|
return ensureReactive(nuxt.payload.data, ssrRef)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows accessing reactive global data that can be synced between server and client.
|
||||||
|
* @param nuxt - (optional) A Nuxt instance
|
||||||
|
*/
|
||||||
|
export function useGlobalData (nuxt = useNuxt()): Record<string, any> {
|
||||||
|
nuxt.payload.data = nuxt.payload.data || {}
|
||||||
|
return nuxt.payload.data
|
||||||
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
export { defineNuxtComponent } from './component'
|
||||||
export { useAsyncData, asyncData } from './asyncData'
|
export { useAsyncData, asyncData } from './asyncData'
|
||||||
export { useData } from './data'
|
export { useData } from './data'
|
||||||
export { useHydration } from './hydrate'
|
export { useHydration } from './hydrate'
|
||||||
|
@ -13,6 +13,8 @@ export interface Nuxt {
|
|||||||
|
|
||||||
[key: string]: any
|
[key: string]: any
|
||||||
|
|
||||||
|
_asyncDataPromises?: Record<string, Promise<any>>
|
||||||
|
|
||||||
ssrContext?: Record<string, any>
|
ssrContext?: Record<string, any>
|
||||||
payload: {
|
payload: {
|
||||||
serverRendered?: true
|
serverRendered?: true
|
||||||
|
37
playground/pages/composables/asyncData/parallel.vue
Normal file
37
playground/pages/composables/asyncData/parallel.vue
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<nuxt-link to="/">
|
||||||
|
Home
|
||||||
|
</nuxt-link>
|
||||||
|
<h2>{{ $route.path }}</h2>
|
||||||
|
<pre>{{ foo }}</pre>
|
||||||
|
<pre>{{ bar }}</pre>
|
||||||
|
<pre>{{ from }}</pre>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { defineNuxtComponent, asyncData } from 'nuxt/app/composables'
|
||||||
|
const waitFor = (ms = 0) => new Promise(resolve => setTimeout(resolve, ms))
|
||||||
|
|
||||||
|
export default defineNuxtComponent({
|
||||||
|
setup () {
|
||||||
|
const { data: foo } = asyncData('foo', async () => {
|
||||||
|
await waitFor(500)
|
||||||
|
return { foo: true }
|
||||||
|
})
|
||||||
|
const { data: bar } = asyncData('bar', async () => {
|
||||||
|
await waitFor(500)
|
||||||
|
return { bar: true }
|
||||||
|
})
|
||||||
|
|
||||||
|
const { data: from } = asyncData('from', () => ({ from: process.server ? 'server' : 'client' }))
|
||||||
|
|
||||||
|
return {
|
||||||
|
foo,
|
||||||
|
bar,
|
||||||
|
from
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
39
playground/pages/composables/asyncData/series.vue
Normal file
39
playground/pages/composables/asyncData/series.vue
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<nuxt-link to="/">
|
||||||
|
Home
|
||||||
|
</nuxt-link>
|
||||||
|
<h2>{{ $route.path }}</h2>
|
||||||
|
<pre>{{ foo }}</pre>
|
||||||
|
<pre>{{ bar }}</pre>
|
||||||
|
<pre>{{ from }}</pre>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { useAsyncData } from 'nuxt/app/composables'
|
||||||
|
const waitFor = (ms = 0) => new Promise(resolve => setTimeout(resolve, ms))
|
||||||
|
|
||||||
|
export default {
|
||||||
|
async setup () {
|
||||||
|
const asyncData = useAsyncData()
|
||||||
|
|
||||||
|
const { data: foo } = await asyncData('foo', async () => {
|
||||||
|
await waitFor(500)
|
||||||
|
return { foo: true }
|
||||||
|
})
|
||||||
|
const { data: bar } = await asyncData('bar', async () => {
|
||||||
|
await waitFor(500)
|
||||||
|
return { bar: true }
|
||||||
|
})
|
||||||
|
|
||||||
|
const { data: from } = await asyncData('from', () => ({ from: process.server ? 'server' : 'client' }))
|
||||||
|
|
||||||
|
return {
|
||||||
|
foo,
|
||||||
|
bar,
|
||||||
|
from
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
@ -1,5 +1,28 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
|
<h2>
|
||||||
Hello world
|
Hello world
|
||||||
|
</h2>
|
||||||
|
<strong>Playground pages</strong>
|
||||||
|
<ul>
|
||||||
|
<li v-for="link of links" :key="link">
|
||||||
|
<nuxt-link :to="link">
|
||||||
|
{{ link }}
|
||||||
|
</nuxt-link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { defineComponent } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
setup () {
|
||||||
|
const links = useRouter().getRoutes().filter(route => ['index', '404'].includes(route.name) === false).map(route => route.path)
|
||||||
|
|
||||||
|
return { links }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
@ -10,6 +10,9 @@
|
|||||||
"types": [
|
"types": [
|
||||||
"node",
|
"node",
|
||||||
"jest"
|
"jest"
|
||||||
]
|
],
|
||||||
|
"paths": {
|
||||||
|
"nuxt/app/composables": ["./packages/app/src/composables"]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user