Skip to content

Commit

Permalink
feat: create shallowSsrRef (#49)
Browse files Browse the repository at this point in the history
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.

Co-authored-by: Sebastian Krüger <2pi_r2@gmx.de>
  • Loading branch information
danielroe and mathe42 authored May 11, 2020
1 parent b6ca977 commit 5ef0f6c
Show file tree
Hide file tree
Showing 9 changed files with 123 additions and 5 deletions.
1 change: 1 addition & 0 deletions docs/.vuepress/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ module.exports = {
title: 'Helpers',
collapsable: false,
children: [
'/helpers/shallowSsrRef',
'/helpers/ssrRef',
'/helpers/useAsync',
'/helpers/useContext',
Expand Down
41 changes: 41 additions & 0 deletions docs/helpers/shallowSsrRef.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
---

# shallowSsrRef

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.

```ts
import { shallowSsrRef, onMounted } from 'nuxt-composition-api'

const shallow = shallowSsrRef({ v: 'init' })
if (process.server) shallow.value = { v: 'changed' }

// On client-side, shallow.value will be { v: changed }
onMounted(() => {
// This and other changes outside of setup won't trigger component updates.
shallow.value.v = 'Hello World'
})
```

::: warning
At the moment, an `shallowSsrRef` is only suitable for one-offs, unless you provide your own unique key.

This is because server and client `shallowSsrRef` matches up based on line number within your code.

```ts
function useMyFeature() {
// Only one unique key is generated
const feature = shallowSsrRef('')
return feature
}

const a = useMyFeature()
const b = useMyFeature()

b.value = 'changed'
// On client-side, a's value will also be initialised to 'changed'
```

If you want to use this pattern, make sure to set a unique key based on each calling of the function.
:::
2 changes: 1 addition & 1 deletion src/babel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export default function ssrRefPlugin({ loadOptions, getEnv, types: t }: Babel) {
CallExpression(path) {
if (
!('name' in path.node.callee) ||
!['ssrRef', 'useAsync'].includes(path.node.callee.name)
!['ssrRef', 'useAsync', 'shallowSsrRef'].includes(path.node.callee.name)
)
return

Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ export { defineComponent } from './component'
export { useContext, withContext } from './context'
export { useFetch } from './fetch'
export { useMeta } from './meta'
export { ssrRef, setSSRContext } from './ssr-ref'
export { ssrRef, shallowSsrRef, setSSRContext } from './ssr-ref'

export type {
ComponentRenderProxy,
Expand Down
58 changes: 58 additions & 0 deletions src/ssr-ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,61 @@ export const ssrRef = <T>(value: T | (() => T), key?: string): Ref<T> => {

return proxy as Ref<T>
}

// TODO: remove when https://github.com/vuejs/composition-api/pull/311 is merged
function shallowRef<T>(value: T): Ref<T> {
return computed({
get: () => value,
set: v => (value = v),
})
}

/**
* 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 `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 { shallowSsrRef, onMounted } from 'nuxt-composition-api'
const shallow = shallowSsrRef({ v: 'init' })
if (process.server) shallow.value = { v: 'changed' }
// On client-side, shallow.value will be { v: changed }
onMounted(() => {
// This and other changes outside of setup won't trigger component updates.
shallow.value.v = 'Hello World'
})
```
*/
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."
)
}

if (process.client) {
return shallowRef(
(window as any).__NUXT__?.ssrRefs?.[key] ?? getValue(value)
)
}

let _val = getValue(value)

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

return computed({
get: () => _val,
set: v => {
data[key] = sanitise(v)
_val = v
},
})
}
4 changes: 2 additions & 2 deletions test/e2e/helpers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ export function navigateTo(path: string) {

export function expectOnPage(text: string) {
const selector = Selector('*').withText(new RegExp(text, 'i'))
return t.expect(selector.visible).ok()
return t.expect(selector.visible).ok(`${text} was not found on page`)
}

export function expectNotOnPage(text: string) {
const selector = Selector('*').withText(new RegExp(text, 'i'))
return t.expect(selector.exists).notOk()
return t.expect(selector.exists).notOk(`${text} was found on page`)
}

export const getWindowPathname = ClientFunction(() => window.location.pathname)
Expand Down
3 changes: 3 additions & 0 deletions test/e2e/ssr-refs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,12 @@ test('Shows data on ssr-loaded page', async t => {
await expectOnPage('function-runs SSR or client-side')
await expectOnPage('prefetched-result')
await expectOnPage('on: server')
await expectOnPage('shallow-server')

await t.click(Selector('a').withText('home'))
await t.click(Selector('a').withText('ssr refs'))
await expectOnPage('ref-only SSR rendered')
await expectOnPage('shallow-client')
})

test('Shows appropriate data on client-loaded page', async t => {
Expand All @@ -28,6 +30,7 @@ test('Shows appropriate data on client-loaded page', async t => {
await expectNotOnPage('ref-only SSR rendered')
await expectOnPage('function-runs SSR or client-side')
await expectOnPage('on: client')
await expectOnPage('shallow-client')
})

test('Shows SSR data when an ssrRef is defined outside setup', async () => {
Expand Down
10 changes: 10 additions & 0 deletions test/fixture/pages/ssr-ref.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<div>prefetched-{{ prefetchValue }}</div>
<div>on: {{ asyncValue }}</div>
<div>no-change: {{ noChange }}</div>
<div>shallow-{{ shallow.v.deep }}</div>
<nuxt-link to="/">home</nuxt-link>
</div>
</template>
Expand All @@ -18,6 +19,8 @@ import {
ssrRef,
onServerPrefetch,
useAsync,
shallowSsrRef,
onMounted,
} from 'nuxt-composition-api'
export function fetcher(result, time = 100) {
Expand All @@ -35,6 +38,12 @@ export default defineComponent({
const funcValue = ssrRef(() => 'runs SSR or client-side') // function => in __NUXT__
const noChange = ssrRef('initValue') // no Change => not in __NUXT__
const shallow = shallowSsrRef({ v: { deep: 'init' } })
if (process.server) shallow.value = { v: { deep: 'server' } }
onMounted(() => {
shallow.value.v.deep = 'client'
})
const computedVal = computed(() => refValue.value)
if (process.server) refValue.value = 'only SSR rendered'
Expand All @@ -58,6 +67,7 @@ export default defineComponent({
prefetchValue,
asyncValue,
noChange,
shallow,
}
},
})
Expand Down
7 changes: 6 additions & 1 deletion test/tsd/ssr-ref.test-d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { expectType } from 'tsd'

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

expectType<Ref<number>>(ssrRef(() => 42))
expectType<Ref<string>>(ssrRef('thoughtless'))
Expand All @@ -9,3 +9,8 @@ interface Obj {
}
expectType<Ref<Obj>>(ssrRef({ name: 'today' }))
expectType<Ref<null>>(ssrRef(null))

expectType<Ref<number>>(shallowSsrRef(() => 42))
expectType<Ref<string>>(shallowSsrRef('thoughtless'))
expectType<Ref<Obj>>(shallowSsrRef({ name: 'today' }))
expectType<Ref<null>>(shallowSsrRef(null))

1 comment on commit 5ef0f6c

@vercel
Copy link

@vercel vercel bot commented on 5ef0f6c May 11, 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.