Skip to content

Commit

Permalink
fix: ensure ssrRef doesn't share state across requests when used in…
Browse files Browse the repository at this point in the history
… `setup()` (#310)

Co-authored-by: Daniel Roe <daniel@roe.dev>
  • Loading branch information
andrzejewsky and danielroe authored Dec 12, 2020
1 parent 3564520 commit 61a3b55
Show file tree
Hide file tree
Showing 7 changed files with 111 additions and 32 deletions.
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()
})
})

1 comment on commit 61a3b55

@vercel
Copy link

@vercel vercel bot commented on 61a3b55 Dec 12, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.