Skip to content
This repository has been archived by the owner on Dec 5, 2024. It is now read-only.

SSR refs breaks the hydration in concurrent requests #310

Merged
merged 5 commits into from
Dec 12, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion docs/content/en/helpers/reqRef.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,16 @@ title: reqRef, reqSsrRef
description: '@nuxtjs/composition-api provides a way to use the Vue 3 Composition API with Nuxt-specific features.'
category: Helpers
fullscreen: True
badge: deprecated
version: 0.133
position: 12
---

`reqRef` declares a normal `ref` with one key difference. It resets the value of this ref on each request. You can find out [more information here](/getting-started/gotchas#shared-server-state).

<alert>You should take especial care because of the danger of shared state when using refs in this way.</alert>
<alert>You do not need a `reqRef` if you are using an `ssrRef` within a component setup function as it will be automatically tied to the per-request state.</alert>

<alert type="warning">You should take especial care because of the danger of shared state when using refs in this way.</alert>


## Example
Expand Down
2 changes: 2 additions & 0 deletions docs/content/en/helpers/ssrRef.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ position: 14

When creating composition utility functions, often there will be server-side state that needs to be conveyed to the client.

<alert>If initialised within `setup()` or via `onGlobalSetup`, `ssrRef` data will exist *only* within the request state. If initialised *outside* a component there is the possibility that an `ssrRef` may share state across requests.</alert>

## ssrRef

`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.
Expand Down
11 changes: 4 additions & 7 deletions src/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,14 @@ export const setMetaPlugin: Plugin = context => {
* @private
*/
export const globalPlugin: Plugin = context => {
const { setup } = context.app
globalSetup = new Set<SetupFunction>()
if (process.server) {
reqRefs.forEach(reset => reset())
setSSRContext(context.app)
}

const { setup } = context.app
globalSetup = new Set<SetupFunction>()

context.app.setup = function (...args) {
let result = {}
if (setup instanceof Function) {
Expand All @@ -60,9 +62,4 @@ export const globalPlugin: Plugin = context => {
}
return result
}

if (!process.server) return
if (context.app.context.ssrContext) {
setSSRContext(context.app.context.ssrContext)
}
}
6 changes: 6 additions & 0 deletions src/req-ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import { sanitise, ssrRef } from './ssr-ref'

export const reqRefs = new Set<() => void>()

/**
* @deprecated
*/
export const reqRef = <T>(initialValue: T): Ref<T> => {
const _ref = ref(initialValue)

Expand All @@ -12,6 +15,9 @@ export const reqRef = <T>(initialValue: T): Ref<T> => {
return _ref as Ref<T>
}

/**
* @deprecated
*/
export const reqSsrRef = <T>(initialValue: T, key?: string) => {
const _ref = ssrRef(initialValue, key)

Expand Down
74 changes: 56 additions & 18 deletions src/ssr-ref.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
customRef,
getCurrentInstance,
onServerPrefetch,
ref,
shallowRef,
Expand All @@ -14,11 +15,39 @@ function getValue<T>(value: T | (() => T)): T {
return value
}

let data: any = {}
let globalRefs: any = {}

export function setSSRContext(ssrContext: any) {
data = Object.assign({}, {})
ssrContext.nuxt.ssrRefs = data
export function setSSRContext(app: any) {
globalRefs = Object.assign({}, {})
app.context.ssrContext.nuxt.globalRefs = globalRefs
}

const useServerData = () => {
let type: 'globalRefs' | 'ssrRefs' = 'globalRefs'

const vm = getCurrentInstance()

if (vm) {
type = 'ssrRefs'
if (process.server) {
const { ssrContext } = vm[globalNuxt].context
;(ssrContext as any).nuxt.ssrRefs = (ssrContext as any).nuxt.ssrRefs || {}
}
}

const setData = (key: string, val: any) => {
switch (type) {
case 'globalRefs':
globalRefs[key] = sanitise(val)
break
case 'ssrRefs':
;(vm![globalNuxt].context.ssrContext as any).nuxt.ssrRefs[
key
] = sanitise(val)
}
}

return { type, setData }
}

const isProxyable = (val: unknown): val is Record<string, unknown> =>
Expand All @@ -27,15 +56,19 @@ const isProxyable = (val: unknown): val is Record<string, unknown> =>
export const sanitise = (val: unknown) =>
(val && JSON.parse(JSON.stringify(val))) || val

const ssrValue = <T>(value: T | (() => T), key: string): T => {
const ssrValue = <T>(
value: T | (() => T),
key: string,
type: 'globalRefs' | 'ssrRefs' = 'globalRefs'
): 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 (window as any)[globalContext]?.[type]?.[key] ?? getValue(value)
}
return getValue(value)
}
Expand All @@ -61,11 +94,14 @@ const ssrValue = <T>(value: T | (() => T), key: string): T => {
*/
export const ssrRef = <T>(value: T | (() => T), key?: string): Ref<T> => {
validateKey(key)
let val = ssrValue(value, key)

const { type, setData } = useServerData()

let val = ssrValue(value, key, type)

if (process.client) return ref(val) as Ref<T>

if (value instanceof Function) data[key] = sanitise(val)
if (value instanceof Function) setData(key, val)

const getProxy = <T extends Record<string | number, any>>(
track: () => void,
Expand All @@ -81,7 +117,7 @@ export const ssrRef = <T>(value: T | (() => T), key?: string): Ref<T> => {
},
set(obj, prop, newVal) {
const result = Reflect.set(obj, prop, newVal)
data[key] = sanitise(val)
setData(key, val)
trigger()
return result
},
Expand All @@ -94,7 +130,7 @@ export const ssrRef = <T>(value: T | (() => T), key?: string): Ref<T> => {
return val
},
set: (v: T) => {
data[key] = sanitise(v)
setData(key, v)
val = v
trigger()
},
Expand All @@ -107,7 +143,7 @@ export const ssrRef = <T>(value: T | (() => T), key?: string): Ref<T> => {
* This helper creates a [`shallowRef`](https://vue-composition-api-rfc.netlify.app/api.html#shallowref) (a ref that tracks its own .value mutation but doesn't make its value reactive) that is synced between client & server.
* @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, `shallowSsrRef` requires a key to ensure that the ref values match between client and server. If you have added `@nuxtjs/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 `@nuxtjs/composition-api/babel` to your Babel plugins.

* @example
```ts
import { shallowSsrRef, onMounted } from '@nuxtjs/composition-api'
Expand All @@ -127,13 +163,14 @@ export const shallowSsrRef = <T>(
key?: string
): Ref<T> => {
validateKey(key)
const { type, setData } = useServerData()

if (process.client) return shallowRef(ssrValue(value, key))
if (process.client) return shallowRef(ssrValue(value, key, type))

const _val = getValue(value)

if (value instanceof Function) {
data[key] = sanitise(_val)
setData(key, _val)
}

return customRef((track, trigger) => ({
Expand All @@ -142,7 +179,7 @@ export const shallowSsrRef = <T>(
return _val
},
set(newValue: T) {
data[key] = sanitise(newValue)
setData(key, newValue)
value = newValue
trigger()
},
Expand All @@ -167,13 +204,13 @@ export const shallowSsrRef = <T>(
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 server, this will be null until the promise resolves.
// On the client, if server-rendered, this will always be the resolved promise.
resolvedPromise,
}
Expand All @@ -186,12 +223,13 @@ export const ssrPromise = <T>(
key?: string
): Promise<T> => {
validateKey(key)
const { type, setData } = useServerData()

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

onServerPrefetch(async () => {
data[key] = sanitise(await val)
setData(key, await val)
})
return val
}
14 changes: 14 additions & 0 deletions test/unit/__snapshots__/ssr-ref.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
exports[`ssrRef reactivity ssrRefs react to change in state 1`] = `
Object {
"nuxt": Object {
"globalRefs": Object {},
"ssrRefs": Object {
"name": "full name",
},
Expand All @@ -13,6 +14,7 @@ Object {
exports[`ssrRef reactivity ssrRefs react to deep change in array state 1`] = `
Object {
"nuxt": Object {
"globalRefs": Object {},
"ssrRefs": Object {
"obj": Object {
"deep": Object {
Expand All @@ -31,6 +33,7 @@ Object {
exports[`ssrRef reactivity ssrRefs react to deep change in object state 1`] = `
Object {
"nuxt": Object {
"globalRefs": Object {},
"ssrRefs": Object {
"obj": Object {
"deep": Object {
Expand All @@ -43,3 +46,14 @@ Object {
},
}
`;

exports[`ssrRef reactivity ssrRefs within multiple requests 1`] = `
Object {
"nuxt": Object {
"globalRefs": Object {},
"ssrRefs": Object {
"concurrentRef": 2,
},
},
}
`;
31 changes: 25 additions & 6 deletions test/unit/ssr-ref.spec.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
/**
* @jest-environment jsdom
*/
import { ssrRef, setSSRContext } from '../..'
import { ssrRef, globalPlugin } from '../..'
import * as cAPI from '@vue/composition-api'

jest.setTimeout(60000)

/* eslint-disable @typescript-eslint/no-var-requires */
const { setup, get } = require('@nuxtjs/module-test-utils')
const config = require('../fixture/nuxt.config')
/* eslint-enable */
import { setup, get } from '@nuxtjs/module-test-utils'
import config from '../fixture/nuxt.config'

let nuxt

Expand Down Expand Up @@ -45,8 +44,14 @@ describe('ssrRef reactivity', () => {
let ssrContext: Record<string, any>

beforeEach(async () => {
process.server = true
ssrContext = Object.assign({}, { nuxt: {} })
setSSRContext(ssrContext)
;(cAPI as any).getCurrentInstance = () => ({
$nuxt: {
context: { ssrContext },
},
})
globalPlugin({ app: { context: { ssrContext } } } as any, null)
})
test('ssrRefs react to change in state', async () => {
process.client = false
Expand All @@ -67,4 +72,18 @@ describe('ssrRef reactivity', () => {
obj.value.deep.object[0].name = 'full name'
expect(ssrContext).toMatchSnapshot()
})

test('ssrRefs within multiple requests', async () => {
const concurrentRef = ssrRef(1, 'concurrentRef')

// simulate the new request comes in
globalPlugin(
{ app: { context: { ssrContext: { nuxt: {} } } } } as any,
null
)

concurrentRef.value = 2

expect(ssrContext).toMatchSnapshot()
})
})