mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-22 13:45:18 +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 { ensureReactive, useData } from './data'
|
||||
import { NuxtComponentPendingPromises } from './component'
|
||||
import { ensureReactive, useGlobalData } from './data'
|
||||
|
||||
export type AsyncDataFn<T> = (ctx?: Nuxt) => Promise<T>
|
||||
|
||||
@ -10,35 +11,36 @@ export interface AsyncDataOptions {
|
||||
defer?: boolean
|
||||
}
|
||||
|
||||
export interface AsyncDataObj<T> {
|
||||
data: Ref<T>
|
||||
export interface AsyncDataState<T> {
|
||||
data: UnwrapRef<T>
|
||||
pending: Ref<boolean>
|
||||
refresh: () => Promise<void>
|
||||
fetch: (force?: boolean) => Promise<UnwrapRef<T>>
|
||||
error?: any
|
||||
}
|
||||
|
||||
export type AsyncDataResult<T> = AsyncDataState<T> & Promise<AsyncDataState<T>>
|
||||
|
||||
export function useAsyncData (defaults?: AsyncDataOptions) {
|
||||
const nuxt = useNuxt()
|
||||
const vm = getCurrentInstance()
|
||||
|
||||
const data = useData(nuxt, vm)
|
||||
let dataRef = 1
|
||||
|
||||
const onMountedCbs: Array<() => void> = []
|
||||
const onBeforeMountCbs: Array<() => void> = []
|
||||
|
||||
if (process.client) {
|
||||
onMounted(() => {
|
||||
onMountedCbs.forEach((cb) => { cb() })
|
||||
onMountedCbs.splice(0, onMountedCbs.length)
|
||||
onBeforeMount(() => {
|
||||
onBeforeMountCbs.forEach((cb) => { cb() })
|
||||
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>,
|
||||
options?: AsyncDataOptions
|
||||
): Promise<AsyncDataObj<T>> {
|
||||
options: AsyncDataOptions = {}
|
||||
): AsyncDataResult<T> {
|
||||
if (typeof handler !== 'function') {
|
||||
throw new TypeError('asyncData handler must be a function')
|
||||
}
|
||||
@ -49,71 +51,77 @@ export function useAsyncData (defaults?: AsyncDataOptions) {
|
||||
...options
|
||||
}
|
||||
|
||||
const key = String(dataRef++)
|
||||
const pending = ref(true)
|
||||
const globalData = useGlobalData(nuxt)
|
||||
|
||||
const datastore = ensureReactive(data, key)
|
||||
|
||||
const fetch = async () => {
|
||||
pending.value = true
|
||||
const _handler = handler(nuxt)
|
||||
|
||||
if (_handler instanceof Promise) {
|
||||
// Let user resolve if request is promise
|
||||
// TODO: handle error
|
||||
const result = await _handler
|
||||
const state = {
|
||||
data: ensureReactive(globalData, key) as UnwrapRef<T>,
|
||||
pending: ref(true)
|
||||
} as AsyncDataState<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) {
|
||||
datastore[_key] = result[_key]
|
||||
}
|
||||
|
||||
pending.value = false
|
||||
} else {
|
||||
// Invalid request
|
||||
throw new TypeError('Invalid asyncData handler: ' + _handler)
|
||||
}
|
||||
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) {
|
||||
// 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.bind(null, nuxt), fetch)
|
||||
}
|
||||
|
||||
// Server side
|
||||
if (process.server && !clientOnly) {
|
||||
await fetch()
|
||||
// 1. Hydration (server: true): no fetch
|
||||
if (nuxt.isHydrating && fetchOnServer) {
|
||||
state.pending.value = false
|
||||
}
|
||||
return {
|
||||
data: datastore,
|
||||
pending,
|
||||
refresh: fetch
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function asyncData<T = Record<string, any>> (
|
||||
handler: AsyncDataFn<T>,
|
||||
options?: AsyncDataOptions
|
||||
): Promise<AsyncDataObj<T>> {
|
||||
return useAsyncData()(handler, options)
|
||||
// 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<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)
|
||||
}
|
||||
|
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 { useNuxt } from '@nuxt/app'
|
||||
|
||||
export function ensureReactive<
|
||||
@ -51,3 +50,12 @@ export function useData<T = Record<string, any>> (
|
||||
|
||||
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 { useData } from './data'
|
||||
export { useHydration } from './hydrate'
|
||||
|
@ -13,6 +13,8 @@ export interface Nuxt {
|
||||
|
||||
[key: string]: any
|
||||
|
||||
_asyncDataPromises?: Record<string, Promise<any>>
|
||||
|
||||
ssrContext?: Record<string, any>
|
||||
payload: {
|
||||
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>
|
||||
<div>
|
||||
<h2>
|
||||
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>
|
||||
</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": [
|
||||
"node",
|
||||
"jest"
|
||||
]
|
||||
],
|
||||
"paths": {
|
||||
"nuxt/app/composables": ["./packages/app/src/composables"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user