Skip to content

Commit

Permalink
feat: add useAsync and improve ssrRef (#28)
Browse files Browse the repository at this point in the history
 - add `useAsync` helper to prefetch data and send in ssrContext to client
 - updates `serverPrefetch` helper
 - improves `ssrRef` handling

Co-authored-by: Sebastian Krüger <2pi_r2@gmx.de>
Co-authored-by: Daniel Roe <daniel@roe.dev>
  • Loading branch information
mathe42 and danielroe authored May 8, 2020
1 parent 96da534 commit 31c9729
Show file tree
Hide file tree
Showing 12 changed files with 161 additions and 37 deletions.
35 changes: 35 additions & 0 deletions src/async.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { ssrRef } from './ssr-ref'
import { onServerPrefetch } from './server-prefetch'
import { Ref, isRef } from '@vue/composition-api'

export const useAsync = <T>(
cb: () => T | Promise<T>,
key?: string | Ref<null>
): Ref<null | T> => {
if (!key) {
throw new Error(
"You must provide a key. You can have it generated automatically by adding 'nuxt-composition-api/babel' to your Babel plugins."
)
}

const _ref = isRef(key) ? key : ssrRef<T | null>(null, key)

if (!_ref.value) {
const p = cb()

if (p instanceof Promise) {
if (process.server) {
onServerPrefetch(async () => {
_ref.value = await p
})
} else {
// eslint-disable-next-line
p.then(res => (_ref.value = res))
}
} else {
_ref.value = p
}
}

return _ref as Ref<null | T>
}
5 changes: 4 additions & 1 deletion src/babel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ export default function ssrRefPlugin({ loadOptions, getEnv, types: t }: Babel) {
}
: {}),
CallExpression(path) {
if (!('name' in path.node.callee) || path.node.callee.name !== 'ssrRef')
if (
!('name' in path.node.callee) ||
!['ssrRef', 'useAsync'].includes(path.node.callee.name)
)
return

if (path.node.arguments.length > 1) return
Expand Down
3 changes: 1 addition & 2 deletions src/fetch.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import Vue from 'vue'
import { getCurrentInstance, onBeforeMount } from '@vue/composition-api'

import { onServerPrefetch } from './ssr-ref'
import { onServerPrefetch } from './server-prefetch'

import { ComponentInstance } from '@vue/composition-api/dist/component'

Expand Down
4 changes: 3 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ export const meta = require('../package.json')

export { useFetch } from './fetch'
export { withContext, useContext } from './context'
export { ssrRef, onServerPrefetch, setSSRContext } from './ssr-ref'
export { ssrRef, setSSRContext } from './ssr-ref'
export { onServerPrefetch } from './server-prefetch'
export { useAsync } from './async'
export { useHead } from './meta'

export {
Expand Down
59 changes: 59 additions & 0 deletions src/server-prefetch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import {
onServerPrefetch as onPrefetch,
getCurrentInstance,
} from '@vue/composition-api'

const ssrRefFunctions = new Map<
ReturnType<typeof getCurrentInstance>,
(() => void)[]
>()

const isPending = new Map<ReturnType<typeof getCurrentInstance>, number>()

let noSetup: Array<() => any> = []

export function onServerPrefetch(cb: () => any) {
const vm = getCurrentInstance()

if (!vm) {
noSetup.push(cb)
return
}

const pending = isPending.get(vm) || 0

isPending.set(vm, pending + 1)

onPrefetch(async () => {
await cb()

const pending = isPending.get(vm) || 0

if (pending <= 1) {
const fn = ssrRefFunctions.get(vm) || []
await Promise.all(fn.map(p => p()))
await Promise.all(noSetup.map(p => p()))
noSetup = []
} else {
isPending.set(vm, pending - 1)
}
})
}

export function onServerPrefetchEnd(cb: () => any) {
if (!process.server) return

const vm = getCurrentInstance()

if (!vm) {
noSetup.push(cb)
} else {
const fn = ssrRefFunctions.get(vm)

if (!fn) {
ssrRefFunctions.set(vm, [cb])
} else {
fn.push(cb)
}
}
}
49 changes: 22 additions & 27 deletions src/ssr-ref.ts
Original file line number Diff line number Diff line change
@@ -1,53 +1,48 @@
import { ref, Ref, onServerPrefetch as prefetch } from '@vue/composition-api'
import { ref, Ref } from '@vue/composition-api'
import { onServerPrefetchEnd } from './server-prefetch'

function getValue<T>(value: T | (() => T)): T {
if (value instanceof Function) return value()
return value
}

let data: any = {}
let injected = false

const refs: Record<string, Ref<any>> = {}

export function setSSRContext(ssrContext: any) {
ssrContext.nuxt.ssrRefs = data
}

export function injectRefs() {
if (!process.server) return

Object.entries(refs).forEach(([key, ref]) => {
data[key] = JSON.parse(JSON.stringify(ref.value))
})
function clone<T>(obj: T): T {
if (typeof obj === 'object') {
return JSON.parse(JSON.stringify(obj))
} else {
return obj
}
}

/**
* Creates a Ref that is in sync with the client.
*/
export const ssrRef = <T>(value: T | (() => T), key?: string): Ref<T> => {
const val = ref<T>(getValue(value))

if (!key)
if (!key) {
throw new Error(
"You must provide a key. You can have it generated automatically by adding 'nuxt-composition-api/babel' to your Babel plugins."
)

if (!injected) {
prefetch(injectRefs)
injected = true
}

if (process.client) {
const nuxtState = (window as any).__NUXT__
val.value = (nuxtState.ssrRefs || {})[key!] ?? getValue(value)
} else {
refs[key] = val
return ref((window as any).__NUXT__?.ssrRefs?.[key] ?? getValue(value))
}

return val as Ref<T>
}
const val = getValue(value)
const initVal = clone(val)
const _ref = ref(val) as Ref<T>

export const onServerPrefetch = (callback: Function) => {
prefetch(async () => {
await callback()
injectRefs()
onServerPrefetchEnd(() => {
if (value instanceof Function || initVal !== _ref.value)
data[key] = _ref.value
})

return _ref

}
2 changes: 2 additions & 0 deletions test/e2e/ssr-refs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ test('Shows data on ssr-loaded page', async t => {
await expectOnPage('ref-only SSR rendered')
await expectOnPage('function-runs SSR or client-side')
await expectOnPage('prefetched-result')
await expectOnPage('on: server')

await t.click(Selector('a').withText('home'))
await t.click(Selector('a').withText('ssr refs'))
Expand All @@ -27,4 +28,5 @@ test.skip('Shows appropriate data on client-loaded page', async t => {
await expectPathnameToBe('/ssr-ref')
await expectNotOnPage('ref-only SSR rendered')
await expectOnPage('function-runs SSR or client-side')
await expectOnPage('on: client')
})
21 changes: 18 additions & 3 deletions test/fixture/pages/ssr-ref.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
<div>ref-{{ computedVal }}</div>
<div>function-{{ funcValue }}</div>
<div>prefetched-{{ prefetchValue }}</div>
<div>on: {{ asyncValue }}</div>
<div>no-change: {{ noChange }}</div>
<nuxt-link to="/">home</nuxt-link>
</div>
</template>
Expand All @@ -15,6 +17,7 @@ import {
useFetch,
ssrRef,
onServerPrefetch,
useAsync,
} from 'nuxt-composition-api'
export function fetcher(result, time = 100) {
Expand All @@ -27,9 +30,10 @@ export function fetcher(result, time = 100) {
export default defineComponent({
setup() {
const refValue = ssrRef('')
const prefetchValue = ssrRef('')
const funcValue = ssrRef(() => 'runs SSR or client-side')
const refValue = ssrRef('') // changed => in __NUXT__
const prefetchValue = ssrRef('') // changed => in __NUXT__
const funcValue = ssrRef(() => 'runs SSR or client-side') // function => in __NUXT__
const noChange = ssrRef('initValue') // no Change => not in __NUXT__
const computedVal = computed(() => refValue.value)
Expand All @@ -39,10 +43,21 @@ export default defineComponent({
prefetchValue.value = await fetcher('result', 500)
})
const asyncValue = useAsync(() =>
fetcher(process.server ? 'server' : 'client', 500)
)
// Error handeling
// useAsync(() => {
// throw '42'
// })
return {
computedVal,
funcValue,
prefetchValue,
asyncValue,
noChange,
}
},
})
Expand Down
9 changes: 9 additions & 0 deletions test/tsd/async.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { expectType } from 'tsd'

import { useAsync, Ref, ssrRef } from '../..'

expectType<Ref<null | { a: string }>>(useAsync(async () => ({ a: 'test' })))

expectType<Ref<null | { a: string }>>(
useAsync(async () => ({ a: 'test' }), ssrRef(null))
)
2 changes: 1 addition & 1 deletion test/tsd/ssr-ref.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import { expectType } from 'tsd'

import { ssrRef, Ref } from '../..'

// eslint-disable-next-line
expectType<Ref<number>>(ssrRef(() => 42))
expectType<Ref<string>>(ssrRef('thoughtless'))
interface Obj {
name: string
}
expectType<Ref<Obj>>(ssrRef({ name: 'today' }))
expectType<Ref<null>>(ssrRef(null))
4 changes: 3 additions & 1 deletion test/unit/__snapshots__/babel-ssr-ref.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,7 @@ exports[`babel plugin works 1`] = `
"const ref = ref(1);
const ref2 = ssrRef(2, \\"ref2-BAW7YFDj4E+Qxr+ujqEADg==\\");
const ref3 = ssrRef(3, 'custom-key');
const ref4 = ssrRef(4, \\"ref4-h6IKM1doqCRBR49lWv2V/g==\\");"
const ref4 = ssrRef(() => 4, \\"ref4-h6IKM1doqCRBR49lWv2V/g==\\");
const async1 = useAsync(() => null, \\"async1-nk1uJ/q39HMoKEMFH33Ryg==\\");
const async2 = useAsync(() => null, 'key');"
`;
5 changes: 4 additions & 1 deletion test/unit/babel-ssr-ref.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ var example = `
const ref = ref(1)
const ref2 = ssrRef(2)
const ref3 = ssrRef(3, 'custom-key')
const ref4 = ssrRef(4)
const ref4 = ssrRef(() => 4)
const async1 = useAsync(() => null)
const async2 = useAsync(() => null, 'key')
`

describe('babel plugin', () => {
Expand Down

0 comments on commit 31c9729

Please sign in to comment.