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 { Ref, ref, onMounted, watch, getCurrentInstance, onUnmounted } from 'vue'
import { Nuxt, useNuxt } from 'nuxt/app' import { Nuxt, useNuxt } from 'nuxt/app'
import { useData } from './data'
import { ensureReactive, useData } from './data'
export type AsyncDataFn<T> = (ctx?: Nuxt) => Promise<T> export type AsyncDataFn<T> = (ctx?: Nuxt) => Promise<T>
@ -12,7 +13,7 @@ export interface AsyncDataOptions {
export interface AsyncDataObj<T> { export interface AsyncDataObj<T> {
data: Ref<T> data: Ref<T>
pending: Ref<boolean> pending: Ref<boolean>
refresh: Function refresh: () => Promise<void>
error?: any error?: any
} }
@ -20,10 +21,10 @@ export function useAsyncData (defaults?: AsyncDataOptions) {
const nuxt = useNuxt() const nuxt = useNuxt()
const vm = getCurrentInstance() const vm = getCurrentInstance()
let data = useData(nuxt, vm) const data = useData(nuxt, vm)
let dataRef = 1 let dataRef = 1
const onMountedCbs = [] const onMountedCbs: Array<() => void> = []
if (process.client) { if (process.client) {
onMounted(() => { onMounted(() => {
@ -31,13 +32,13 @@ export function useAsyncData (defaults?: AsyncDataOptions) {
onMountedCbs.splice(0, onMountedCbs.length) onMountedCbs.splice(0, onMountedCbs.length)
}) })
onUnmounted(() => { onUnmounted(() => onMountedCbs.splice(0, onMountedCbs.length))
onMountedCbs.splice(0, onMountedCbs.length)
data = null
})
} }
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') { if (typeof handler !== 'function') {
throw new TypeError('asyncData handler must be a function') throw new TypeError('asyncData handler must be a function')
} }
@ -51,6 +52,8 @@ export function useAsyncData (defaults?: AsyncDataOptions) {
const key = String(dataRef++) const key = String(dataRef++)
const pending = ref(true) const pending = ref(true)
const datastore = ensureReactive(data, key)
const fetch = async () => { const fetch = async () => {
pending.value = true pending.value = true
const _handler = handler(nuxt) const _handler = handler(nuxt)
@ -59,11 +62,11 @@ export function useAsyncData (defaults?: AsyncDataOptions) {
// Let user resolve if request is promise // Let user resolve if request is promise
// TODO: handle error // TODO: handle error
const result = await _handler const result = await _handler
if (!data[key]) {
data[key] = result for (const _key in result) {
} else { datastore[_key] = result[_key]
Object.assign(data[key], result)
} }
pending.value = false pending.value = false
} else { } else {
// Invalid request // Invalid request
@ -81,14 +84,10 @@ export function useAsyncData (defaults?: AsyncDataOptions) {
} }
// 2. Initial load (server: false): fetch on mounted // 2. Initial load (server: false): fetch on mounted
if (nuxt.isHydrating && !options.server) { if (nuxt.isHydrating && !options.server) {
// Force tracking it
data[key] = {}
// Fetch on mounted (initial load or deferred fetch) // Fetch on mounted (initial load or deferred fetch)
onMountedCbs.push(fetch) onMountedCbs.push(fetch)
} else if (!nuxt.isHydrating) { } else if (!nuxt.isHydrating) {
if (options.defer) { if (options.defer) {
// Force tracking it
data[key] = {}
// 3. Navigation (defer: true): fetch on mounted // 3. Navigation (defer: true): fetch on mounted
onMountedCbs.push(fetch) onMountedCbs.push(fetch)
} else { } else {
@ -105,13 +104,16 @@ export function useAsyncData (defaults?: AsyncDataOptions) {
await fetch() await fetch()
} }
return { return {
data: data[key], data: datastore,
pending, pending,
refresh: fetch 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) 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' 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. * Returns a unique string suitable for syncing data between server and client.
* @param nuxt (optional) A Nuxt instance * @param nuxt (optional) A Nuxt instance
* @param vm (optional) A Vue component - by default it will use the current instance * @param vm (optional) A Vue component - by default it will use the current instance
*/ */
export function useSSRRef (nuxt = useNuxt(), vm = getCurrentInstance()): string { 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 (process.server) {
if (!vm.attrs['data-ssr-ref']) { if (!vm.attrs['data-ssr-ref']) {
nuxt._refCtr = nuxt._refCtr || 1 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 return vm.attrs['data-ssr-ref'] as string
} }
if (process.client) { // Client
return vm.vnode.el?.dataset?.ssrRef || String(Math.random()) /* TODO: unique value for multiple calls */ /* 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 nuxt (optional) A Nuxt instance
* @param vm (optional) A Vue component - by default it will use the current 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) const ssrRef = useSSRRef(nuxt, vm)
nuxt.payload.data = nuxt.payload.data || {} nuxt.payload.data = nuxt.payload.data || {}
if (!isReactive(nuxt.payload.data[ssrRef])) { return ensureReactive(nuxt.payload.data, ssrRef)
nuxt.payload.data[ssrRef] = reactive(nuxt.payload.data[ssrRef] || {})
}
return nuxt.payload.data[ssrRef]
} }

View File

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

View File

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