fix(app): improve composables (#183)

This commit is contained in:
Daniel Roe 2021-03-17 09:17:18 +00:00 committed by GitHub
parent 960523c4b8
commit 451fc29b60
4 changed files with 57 additions and 40 deletions

View File

@ -1,6 +1,7 @@
import { Ref, ref, onMounted, watch, getCurrentInstance, onUnmounted } from 'vue'
import { Nuxt, useNuxt } from 'nuxt/app'
import { useData } from './data'
import { ensureReactive, useData } from './data'
export type AsyncDataFn<T> = (ctx?: Nuxt) => Promise<T>
@ -12,7 +13,7 @@ export interface AsyncDataOptions {
export interface AsyncDataObj<T> {
data: Ref<T>
pending: Ref<boolean>
refresh: Function
refresh: () => Promise<void>
error?: any
}
@ -20,10 +21,10 @@ export function useAsyncData (defaults?: AsyncDataOptions) {
const nuxt = useNuxt()
const vm = getCurrentInstance()
let data = useData(nuxt, vm)
const data = useData(nuxt, vm)
let dataRef = 1
const onMountedCbs = []
const onMountedCbs: Array<() => void> = []
if (process.client) {
onMounted(() => {
@ -31,13 +32,13 @@ export function useAsyncData (defaults?: AsyncDataOptions) {
onMountedCbs.splice(0, onMountedCbs.length)
})
onUnmounted(() => {
onMountedCbs.splice(0, onMountedCbs.length)
data = null
})
onUnmounted(() => onMountedCbs.splice(0, onMountedCbs.length))
}
return async function asyncData<T = any> (handler: AsyncDataFn<T>, options?: AsyncDataOptions): Promise<AsyncDataObj<T>> {
return async function asyncData<T = Record<string, any>> (
handler: AsyncDataFn<T>,
options?: AsyncDataOptions
): Promise<AsyncDataObj<T>> {
if (typeof handler !== 'function') {
throw new TypeError('asyncData handler must be a function')
}
@ -51,6 +52,8 @@ export function useAsyncData (defaults?: AsyncDataOptions) {
const key = String(dataRef++)
const pending = ref(true)
const datastore = ensureReactive(data, key)
const fetch = async () => {
pending.value = true
const _handler = handler(nuxt)
@ -59,11 +62,11 @@ export function useAsyncData (defaults?: AsyncDataOptions) {
// Let user resolve if request is promise
// TODO: handle error
const result = await _handler
if (!data[key]) {
data[key] = result
} else {
Object.assign(data[key], result)
for (const _key in result) {
datastore[_key] = result[_key]
}
pending.value = false
} else {
// Invalid request
@ -81,14 +84,10 @@ export function useAsyncData (defaults?: AsyncDataOptions) {
}
// 2. Initial load (server: false): fetch on mounted
if (nuxt.isHydrating && !options.server) {
// Force tracking it
data[key] = {}
// Fetch on mounted (initial load or deferred fetch)
onMountedCbs.push(fetch)
} else if (!nuxt.isHydrating) {
if (options.defer) {
// Force tracking it
data[key] = {}
// 3. Navigation (defer: true): fetch on mounted
onMountedCbs.push(fetch)
} else {
@ -105,13 +104,16 @@ export function useAsyncData (defaults?: AsyncDataOptions) {
await fetch()
}
return {
data: data[key],
data: datastore,
pending,
refresh: fetch
}
}
}
export function asyncData<T = any> (handler: AsyncDataFn<T>, options?: AsyncDataOptions): Promise<AsyncDataObj<T>> {
export function asyncData<T = Record<string, any>> (
handler: AsyncDataFn<T>,
options?: AsyncDataOptions
): Promise<AsyncDataObj<T>> {
return useAsyncData()(handler, options)
}

View File

@ -1,12 +1,28 @@
import { getCurrentInstance, reactive, isReactive } from 'vue'
import { getCurrentInstance, isReactive, reactive, UnwrapRef } from 'vue'
import { useNuxt } from 'nuxt/app'
export function ensureReactive<
T extends Record<string, any>,
K extends keyof T
> (data: T, key: K): UnwrapRef<T[K]> {
if (!isReactive(data[key])) {
data[key] = reactive(data[key] || ({} as T[K]))
}
return data[key]
}
/**
* Returns a unique string suitable for syncing data between server and client.
* @param nuxt (optional) A Nuxt instance
* @param vm (optional) A Vue component - by default it will use the current instance
*/
export function useSSRRef (nuxt = useNuxt(), vm = getCurrentInstance()): string {
if (!vm) {
throw new Error('This must be called within a setup function.')
}
// Server
if (process.server) {
if (!vm.attrs['data-ssr-ref']) {
nuxt._refCtr = nuxt._refCtr || 1
@ -15,9 +31,9 @@ export function useSSRRef (nuxt = useNuxt(), vm = getCurrentInstance()): string
return vm.attrs['data-ssr-ref'] as string
}
if (process.client) {
return vm.vnode.el?.dataset?.ssrRef || String(Math.random()) /* TODO: unique value for multiple calls */
}
// Client
/* TODO: unique value for multiple calls */
return vm.vnode.el?.dataset?.ssrRef || String(Math.random())
}
/**
@ -25,14 +41,13 @@ export function useSSRRef (nuxt = useNuxt(), vm = getCurrentInstance()): string
* @param nuxt (optional) A Nuxt instance
* @param vm (optional) A Vue component - by default it will use the current instance
*/
export function useData (nuxt = useNuxt(), vm = getCurrentInstance()): ReturnType<typeof reactive> {
export function useData<T = Record<string, any>> (
nuxt = useNuxt(),
vm = getCurrentInstance()
): UnwrapRef<T> {
const ssrRef = useSSRRef(nuxt, vm)
nuxt.payload.data = nuxt.payload.data || {}
if (!isReactive(nuxt.payload.data[ssrRef])) {
nuxt.payload.data[ssrRef] = reactive(nuxt.payload.data[ssrRef] || {})
}
return nuxt.payload.data[ssrRef]
return ensureReactive(nuxt.payload.data, ssrRef)
}

View File

@ -1,9 +1,9 @@
import { getCurrentInstance } from 'vue'
import type { Nuxt } from 'nuxt/app'
let currentNuxtInstance: Nuxt
let currentNuxtInstance: Nuxt | null
export const setNuxtInstance = (nuxt: Nuxt) => {
export const setNuxtInstance = (nuxt: Nuxt | null) => {
currentNuxtInstance = nuxt
}
@ -15,21 +15,20 @@ export const setNuxtInstance = (nuxt: Nuxt) => {
export async function callWithNuxt (nuxt: Nuxt, setup: () => any) {
setNuxtInstance(nuxt)
const p = setup()
setNuxtInstance(undefined)
setNuxtInstance(null)
await p
}
/**
* Returns the current Nuxt instance.
*/
export function useNuxt () {
export function useNuxt (): Nuxt {
const vm = getCurrentInstance()
if (!vm && !currentNuxtInstance) {
throw new Error('nuxt instance unavailable')
}
if (!vm) {
if (!currentNuxtInstance) {
throw new Error('nuxt instance unavailable')
}
return currentNuxtInstance
}

View File

@ -15,8 +15,8 @@ export interface Nuxt {
ssrContext?: Record<string, any>
payload: {
serverRendered?: true,
data?: object
serverRendered?: true
data?: Record<string, any>
rendered?: Function
[key: string]: any
}
@ -36,7 +36,6 @@ export interface CreateOptions {
export function createNuxt (options: CreateOptions) {
const nuxt: Nuxt = {
app: undefined,
provide: undefined,
globalName: 'nuxt',
state: {},
@ -69,6 +68,8 @@ export function createNuxt (options: CreateOptions) {
serverRendered: true // TODO: legacy
}
nuxt.ssrContext = nuxt.ssrContext || {}
// Expose to server renderer to create window.__NUXT__
nuxt.ssrContext.payload = nuxt.payload
}