Skip to content

Commit

Permalink
feat: add ssrPromise functionality
Browse files Browse the repository at this point in the history
closes #115 - feat: a promise returning function to transfer state between server and client
  • Loading branch information
danielroe committed Jun 13, 2020
1 parent df5653c commit 461939d
Show file tree
Hide file tree
Showing 11 changed files with 184 additions and 36 deletions.
1 change: 1 addition & 0 deletions docs/.vuepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ module.exports = {
children: [
'/helpers/onGlobalSetup',
'/helpers/shallowSsrRef',
'/helpers/ssrPromise',
'/helpers/ssrRef',
'/helpers/useAsync',
'/helpers/useContext',
Expand Down
40 changes: 40 additions & 0 deletions docs/helpers/ssrPromise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
---

# ssrPromise

`ssrPromise` runs a promise on the server and serialises the result as a resolved promise for the client. It needs to be run within the `setup()` function but note that it returns a promise which will require special handling. (For example, you cannot just return a promise from setup and use it in the template.)

```ts
import {
defineComponent,
onBeforeMount,
ref,
ssrPromise,
} from 'nuxt-composition-api'

export default defineComponent({
setup() {
const _promise = ssrPromise(async () => myAsyncFunction())
const resolvedPromise = ref(null)

onBeforeMount(async () => {
resolvedPromise.value = await _promise
})

return {
// On the server, this will be null until the promise resolves.
// On the client, if server-rendered, this will always be the resolved promise.
resolvedPromise,
}
},
})
```

::: tip
Under the hood, `ssrPromise` requires a key to ensure that the ref values match between client and server. If you have added `nuxt-composition-api` to your `buildModules`, this will be done automagically by an injected Babel plugin. If you need to do things differently, you can specify a key manually or add `nuxt-composition-api/babel` to your Babel plugins.
:::

::: warning
At the moment, an `ssrPromise` is only suitable for one-offs, unless you provide your own unique key. See [the `ssrRef` documentation](./ssrRef.md) for more information.
:::
7 changes: 2 additions & 5 deletions src/async.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { Ref, UnwrapRef } from '@vue/composition-api'

import { globalNuxt } from './globals'
import { ssrRef } from './ssr-ref'
import { validateKey } from './utils'

/**
* You can create reactive values that depend on asynchronous calls with `useAsync`.
Expand Down Expand Up @@ -32,11 +33,7 @@ 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."
)
}
validateKey(key)

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

Expand Down
3 changes: 2 additions & 1 deletion src/babel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,9 @@ export default function ssrRefPlugin({ loadOptions, getEnv, types: t }: Babel) {
method = 'hex'
break

case 'ssrRef':
case 'shallowSsrRef':
case 'ssrPromise':
case 'ssrRef':
case 'useAsync':
if (path.node.arguments.length > 1) return
break
Expand Down
2 changes: 1 addition & 1 deletion src/entrypoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export { useContext, withContext } from './context'
export { useFetch } from './fetch'
export { globalPlugin, onGlobalSetup } from './hooks'
export { useMeta } from './meta'
export { ssrRef, shallowSsrRef, setSSRContext } from './ssr-ref'
export { ssrRef, shallowSsrRef, setSSRContext, ssrPromise } from './ssr-ref'
export { useStatic } from './static'

export type {
Expand Down
94 changes: 71 additions & 23 deletions src/ssr-ref.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import { ref, computed, shallowRef } from '@vue/composition-api'
import {
computed,
onServerPrefetch,
ref,
shallowRef,
} from '@vue/composition-api'
import type { Ref, UnwrapRef } from '@vue/composition-api'

import { globalContext, globalNuxt } from './globals'
import { validateKey } from './utils'

function getValue<T>(value: T | (() => T)): T {
if (value instanceof Function) return value()
Expand All @@ -21,6 +27,19 @@ const isProxyable = (val: unknown): val is Record<string, unknown> =>
const sanitise = (val: unknown) =>
(val && JSON.parse(JSON.stringify(val))) || val

const ssrValue = <T>(value: T | (() => T), key: string): T => {
if (process.client) {
if (
process.env.NODE_ENV === 'development' &&
window[globalNuxt]?.context.isHMR
) {
return getValue(value)
}
return (window as any)[globalContext]?.ssrRefs?.[key] ?? getValue(value)
}
return getValue(value)
}

/**
* `ssrRef` will automatically add ref values to `window.__NUXT__` on SSR if they have been changed from their initial value. It can be used outside of components, such as in shared utility functions, and it supports passing a factory function that will generate the initial value of the ref. **At the moment, an `ssrRef` is only suitable for one-offs, unless you provide your own unique key.**
* @param value This can be an initial value or a factory function that will be executed on server-side to get the initial value.
Expand All @@ -44,25 +63,11 @@ export const ssrRef = <T>(
value: T | (() => T),
key?: string
): Ref<UnwrapRef<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."
)
}
validateKey(key)
const val = ssrValue(value, key)

if (process.client) {
if (
process.env.NODE_ENV === 'development' &&
window[globalNuxt]?.context.isHMR
) {
return ref(getValue(value))
}
return ref(
(window as any)[globalContext]?.ssrRefs?.[key] ?? getValue(value)
)
}
if (process.client) return ref(val)

const val = getValue(value)
const _ref = ref(val)

if (value instanceof Function) data[key] = sanitise(val)
Expand Down Expand Up @@ -114,11 +119,7 @@ export const shallowSsrRef = <T>(
value: T | (() => T),
key?: string
): Ref<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."
)
}
validateKey(key)

if (process.client) {
if (
Expand Down Expand Up @@ -146,3 +147,50 @@ export const shallowSsrRef = <T>(
},
})
}

/**
* `ssrPromise` runs a promise on the server and serialises the result as a resolved promise for the client. It needs to be run within the `setup()` function but note that it returns a promise which will require special handling. (For example, you cannot just return a promise from setup and use it in the template.)
* @param value This can be an initial value or a factory function that will be executed on server-side to get the initial value.
* @param key Under the hood, `ssrPromise` requires a key to ensure that the ref values match between client and server. If you have added `nuxt-composition-api` to your `buildModules`, this will be done automagically by an injected Babel plugin. If you need to do things differently, you can specify a key manually or add `nuxt-composition-api/babel` to your Babel plugins.
* @example
```ts
import {
defineComponent,
onBeforeMount,
ref,
ssrPromise,
} from 'nuxt-composition-api'
export default defineComponent({
setup() {
const _promise = ssrPromise(async () => myAsyncFunction())
const resolvedPromise = ref(null)
onBeforeMount(async () => {
resolvedPromise.value = await _promise
})
return {
// On the server, this will be null until the promise resolves.
// On the client, if server-rendered, this will always be the resolved promise.
resolvedPromise,
}
},
})
```
*/
export const ssrPromise = <T>(
value: () => Promise<T>,
key?: string
): Promise<T> => {
validateKey(key)

const val = ssrValue(value, key)
if (process.client) return Promise.resolve(val)

onServerPrefetch(async () => {
data[key] = sanitise(await val)
})
return val
}
7 changes: 7 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export function validateKey<T>(key?: T): asserts key is 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."
)
}
}
20 changes: 20 additions & 0 deletions test/e2e/promise.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Selector } from 'testcafe'
import { navigateTo, expectOnPage } from './helpers'

// eslint-disable-next-line
fixture`ssrPromise`

test('Shows data on ssr-loaded page', async t => {
await navigateTo('/promise')
await expectOnPage('promise-server')

await t.click(Selector('a').withText('home'))
await t.click(Selector('a').withText('promise'))
await expectOnPage('promise-server')
})

test('Shows appropriate data on client-loaded page', async t => {
await navigateTo('/')
await t.click(Selector('a').withText('promise'))
await expectOnPage('promise-client')
})
1 change: 1 addition & 0 deletions test/fixture/pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
<ul>
<li><nuxt-link to="/other">link forward</nuxt-link></li>
<li><nuxt-link to="/ssr-ref">ssr refs</nuxt-link></li>
<li><nuxt-link to="/promise">promise</nuxt-link></li>
<li><nuxt-link to="/context/a">context</nuxt-link></li>
<li><nuxt-link to="/hooks">hooks</nuxt-link></li>
<li><nuxt-link to="/static/1">static</nuxt-link></li>
Expand Down
33 changes: 33 additions & 0 deletions test/fixture/pages/promise.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<template>
<blockquote>
<p>
<code>promise-{{ promise }}</code>
</p>
</blockquote>
</template>

<script>
import {
defineComponent,
onBeforeMount,
ref,
ssrPromise,
} from 'nuxt-composition-api'
import { fetcher } from '../utils'
export default defineComponent({
setup() {
const _promise = ssrPromise(async () => fetcher(process.server ? 'server' : 'client'))
const promise = ref('')
onBeforeMount(async () => {
promise.value = await _promise
})
return {
promise,
}
},
})
</script>
12 changes: 6 additions & 6 deletions test/fixture/pages/ssr-ref.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,15 @@

<script>
import {
computed,
defineComponent,
onMounted,
onServerPrefetch,
ref,
computed,
useFetch,
shallowSsrRef,
ssrRef,
onServerPrefetch,
useAsync,
shallowSsrRef,
onMounted,
useFetch,
} from 'nuxt-composition-api'
import { fetcher } from '../utils'
Expand All @@ -45,7 +45,7 @@ export default defineComponent({
const shallow = shallowSsrRef({ v: { deep: 'init' } })
if (process.server) shallow.value = { v: { deep: 'server' } }
onMounted(() => {
onMounted(async () => {
shallow.value.v.deep = 'client'
})
Expand Down

0 comments on commit 461939d

Please sign in to comment.