diff --git a/docs/content/en/helpers/reqRef.md b/docs/content/en/helpers/reqRef.md index e837bf13..f20d78c0 100644 --- a/docs/content/en/helpers/reqRef.md +++ b/docs/content/en/helpers/reqRef.md @@ -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). -You should take especial care because of the danger of shared state when using refs in this way. +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. + +You should take especial care because of the danger of shared state when using refs in this way. ## Example diff --git a/docs/content/en/helpers/ssrRef.md b/docs/content/en/helpers/ssrRef.md index b1f05791..b5c929d7 100644 --- a/docs/content/en/helpers/ssrRef.md +++ b/docs/content/en/helpers/ssrRef.md @@ -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. +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. + ## 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. diff --git a/src/hooks.ts b/src/hooks.ts index deefd19a..0406f2af 100644 --- a/src/hooks.ts +++ b/src/hooks.ts @@ -44,12 +44,14 @@ export const setMetaPlugin: Plugin = context => { * @private */ export const globalPlugin: Plugin = context => { - const { setup } = context.app - globalSetup = new Set() if (process.server) { reqRefs.forEach(reset => reset()) + setSSRContext(context.app) } + const { setup } = context.app + globalSetup = new Set() + context.app.setup = function (...args) { let result = {} if (setup instanceof Function) { @@ -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) - } } diff --git a/src/req-ref.ts b/src/req-ref.ts index b5ada4f4..2d69752d 100644 --- a/src/req-ref.ts +++ b/src/req-ref.ts @@ -4,6 +4,9 @@ import { sanitise, ssrRef } from './ssr-ref' export const reqRefs = new Set<() => void>() +/** + * @deprecated + */ export const reqRef = (initialValue: T): Ref => { const _ref = ref(initialValue) @@ -12,6 +15,9 @@ export const reqRef = (initialValue: T): Ref => { return _ref as Ref } +/** + * @deprecated + */ export const reqSsrRef = (initialValue: T, key?: string) => { const _ref = ssrRef(initialValue, key) diff --git a/src/ssr-ref.ts b/src/ssr-ref.ts index 455db019..84273c62 100644 --- a/src/ssr-ref.ts +++ b/src/ssr-ref.ts @@ -1,5 +1,6 @@ import { customRef, + getCurrentInstance, onServerPrefetch, ref, shallowRef, @@ -14,11 +15,39 @@ function getValue(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 => @@ -27,7 +56,11 @@ const isProxyable = (val: unknown): val is Record => export const sanitise = (val: unknown) => (val && JSON.parse(JSON.stringify(val))) || val -const ssrValue = (value: T | (() => T), key: string): T => { +const ssrValue = ( + value: T | (() => T), + key: string, + type: 'globalRefs' | 'ssrRefs' = 'globalRefs' +): T => { if (process.client) { if ( process.env.NODE_ENV === 'development' && @@ -35,7 +68,7 @@ const ssrValue = (value: T | (() => T), key: string): T => { ) { return getValue(value) } - return (window as any)[globalContext]?.ssrRefs?.[key] ?? getValue(value) + return (window as any)[globalContext]?.[type]?.[key] ?? getValue(value) } return getValue(value) } @@ -61,11 +94,14 @@ const ssrValue = (value: T | (() => T), key: string): T => { */ export const ssrRef = (value: T | (() => T), key?: string): Ref => { 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 - if (value instanceof Function) data[key] = sanitise(val) + if (value instanceof Function) setData(key, val) const getProxy = >( track: () => void, @@ -81,7 +117,7 @@ export const ssrRef = (value: T | (() => T), key?: string): Ref => { }, set(obj, prop, newVal) { const result = Reflect.set(obj, prop, newVal) - data[key] = sanitise(val) + setData(key, val) trigger() return result }, @@ -94,7 +130,7 @@ export const ssrRef = (value: T | (() => T), key?: string): Ref => { return val }, set: (v: T) => { - data[key] = sanitise(v) + setData(key, v) val = v trigger() }, @@ -107,7 +143,7 @@ export const ssrRef = (value: T | (() => T), key?: string): Ref => { * 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' @@ -127,13 +163,14 @@ export const shallowSsrRef = ( key?: string ): Ref => { 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) => ({ @@ -142,7 +179,7 @@ export const shallowSsrRef = ( return _val }, set(newValue: T) { - data[key] = sanitise(newValue) + setData(key, newValue) value = newValue trigger() }, @@ -167,13 +204,13 @@ export const shallowSsrRef = ( 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, } @@ -186,12 +223,13 @@ export const ssrPromise = ( key?: string ): Promise => { 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 } diff --git a/test/unit/__snapshots__/ssr-ref.spec.ts.snap b/test/unit/__snapshots__/ssr-ref.spec.ts.snap index 9e9089b3..34af58c0 100644 --- a/test/unit/__snapshots__/ssr-ref.spec.ts.snap +++ b/test/unit/__snapshots__/ssr-ref.spec.ts.snap @@ -3,6 +3,7 @@ exports[`ssrRef reactivity ssrRefs react to change in state 1`] = ` Object { "nuxt": Object { + "globalRefs": Object {}, "ssrRefs": Object { "name": "full name", }, @@ -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 { @@ -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 { @@ -43,3 +46,14 @@ Object { }, } `; + +exports[`ssrRef reactivity ssrRefs within multiple requests 1`] = ` +Object { + "nuxt": Object { + "globalRefs": Object {}, + "ssrRefs": Object { + "concurrentRef": 2, + }, + }, +} +`; diff --git a/test/unit/ssr-ref.spec.ts b/test/unit/ssr-ref.spec.ts index 7141e98a..35dbbfab 100644 --- a/test/unit/ssr-ref.spec.ts +++ b/test/unit/ssr-ref.spec.ts @@ -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 @@ -45,8 +44,14 @@ describe('ssrRef reactivity', () => { let ssrContext: Record 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 @@ -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() + }) })