Skip to content
Closed
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
96 changes: 95 additions & 1 deletion packages/reactivity/__tests__/ref.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,13 @@ import {
isReactive
} from '../src/index'
import { computed } from '@vue/runtime-dom'
import { shallowRef, unref, customRef, triggerRef } from '../src/ref'
import {
shallowRef,
unref,
customRef,
triggerRef,
toShallowRef
} from '../src/ref'
import {
isReadonly,
isShallow,
Expand Down Expand Up @@ -442,4 +448,92 @@ describe('reactivity/ref', () => {
expect(a.value).toBe(rr)
expect(a.value).not.toBe(r)
})

test('toShallowRef should behave like shallowRef', () => {
const sref = toShallowRef({ a: 1 })
expect(isReactive(sref.value)).toBe(false)

let dummy
effect(() => {
dummy = sref.value.a
})
expect(dummy).toBe(1)

sref.value = { a: 2 }
expect(isReactive(sref.value)).toBe(false)
expect(dummy).toBe(2)
})

test('toShallowRef', () => {
const original = {
x: 1,
nested: {
y: 2
}
}
const nested = toShallowRef({ ...original }, 'nested')
expect(isRef(nested)).toBe(true)
expect(isShallow(nested)).toBe(true)

const sref = toShallowRef(ref({ ...original }))
expect(isRef(sref)).toBe(true)
expect(isShallow(sref)).toBe(true)

const ss = toShallowRef(sref)
expect(ss).toBe(sref)

const sr = toShallowRef(reactive({ ...original }))
expect(isRef(sr)).toBe(true)
expect(isReactive(sr)).toBe(false)
expect(isShallow(sr)).toBe(true)

// reactivity nested
let dummyNested
effect(() => {
dummyNested = nested.value.y
})
expect(dummyNested).toBe(nested.value.y)

// mutating source should not trigger effect using the proxy refs
nested.value.y = 4
expect(dummyNested).toBe(2)

// force trigger
triggerRef(nested)
expect(dummyNested).toBe(4)

// reactivity ref
let dummyRef
effect(() => {
dummyRef = sref.value.x
})
expect(dummyRef).toBe(sref.value.x)

// mutating source should not trigger effect using the proxy refs
sref.value.x = 4
expect(dummyRef).toBe(1)

// force trigger
triggerRef(sref)
expect(dummyRef).toBe(4)

// reactivity
let dummyR
effect(() => {
dummyR = sr.value.x
})
expect(dummyR).toBe(sr.value.x)

// mutating source should not trigger effect using the proxy refs
sr.value.x = 4
expect(dummyR).toBe(1)

// force trigger
triggerRef(sr)
expect(dummyR).toBe(4)

// should keep ref
const r = { x: shallowRef(1) }
expect(toShallowRef(r, 'x')).toBe(r.x)
})
})
4 changes: 3 additions & 1 deletion packages/reactivity/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export {
proxyRefs,
customRef,
triggerRef,
toShallowRef,
type Ref,
type MaybeRef,
type MaybeRefOrGetter,
Expand All @@ -18,7 +19,8 @@ export {
type ShallowRef,
type ShallowUnwrapRef,
type RefUnwrapBailTypes,
type CustomRefFactory
type CustomRefFactory,
type ToShallowRef
} from './ref'
export {
reactive,
Expand Down
111 changes: 109 additions & 2 deletions packages/reactivity/src/ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ import {
isReactive,
toReactive,
isReadonly,
isShallow
isShallow,
shallowReactive
} from './reactive'
import type { ShallowReactiveMarker } from './reactive'
import { CollectionTypes } from './collectionHandlers'
Expand Down Expand Up @@ -340,7 +341,8 @@ class ObjectRefImpl<T extends object, K extends keyof T> {
constructor(
private readonly _object: T,
private readonly _key: K,
private readonly _defaultValue?: T[K]
private readonly _defaultValue?: T[K],
public readonly __v_isShallow = false
) {}

get value() {
Expand Down Expand Up @@ -454,6 +456,111 @@ function propertyToRef(
: (new ObjectRefImpl(source, key, defaultValue) as any)
}

export type ToShallowRef<T> = IfAny<
T,
ShallowRef<T>,
[T] extends [ShallowRef] ? T : ShallowRef<T>
>

/**
* Used to normalize values / refs / getters into shallow refs.
*
* @example
* ```js
* // returns existing shallow refs as-is
* toShallowRef(existingShallowRef)
*
* // convert refs to shallow refs
* toShallowRef(existingRef)
*
* // creates a ref that calls the getter on .value access
* toShallowRef(() => props.foo)
*
* // creates shadow refs from non-function values
* // equivalent to shallowRef(1)
* toShallowRef(1)
* ```
*
* Can also be used to create a shallow ref for a property on a source reactive object.
* The created ref is synced with its source property: mutating the source
* property will update the ref, and vice-versa.
*
* @example
* ```js
* const state = reactive({
* nested: {
* foo: 1
* }
* })
*
* const nestedShallowRef = toShallowRef(state, 'nested')
* const doubleFoo = computed(() => nestedShallowRef.value.foo * 2)
*
* // mutating the shallow ref does NOT update the original state
* nestedShallowRef.value.nested.foo = 4
* console.log(doubleFoo.value) // 2
*
* // manual trigger is needed to "sync" the ref
* triggerRef(nestedShallowRef)
* console.log(doubleFoo.value) // 8
* ```
*
* @param source - A getter, an existing ref, a non-function value, or a
* reactive object to create a property shallow ref from.
* @param [key] - (optional) Name of the property in the reactive object.
* @see {@link https://vuejs.org/api/reactivity-utilities.html#toref}
*/
export function toShallowRef<T>(
value: T
): T extends () => infer R
? Readonly<ShallowRef<R>>
: T extends ShallowRef
? T
: ShallowRef<ShallowUnwrapRef<T>>
export function toShallowRef<T extends object, K extends keyof T>(
object: T,
key: K
): ToShallowRef<T[K]>
export function toShallowRef<T extends object, K extends keyof T>(
object: T,
key: K,
defaultValue: T[K]
): ToShallowRef<Exclude<T[K], undefined>>
export function toShallowRef(
source: Record<string, any> | MaybeRef,
key?: string,
defaultValue?: unknown
): ShallowRef {
if (isRef(source)) {
if (isShallow(source)) {
return source
}
return shallowRef(toRaw(source.value))
} else if (isFunction(source)) {
return new GetterRefImpl(source) as any
} else if (isObject(source) && arguments.length > 1) {
return propertyToShallowRef(source, key!, defaultValue)
} else {
return shallowRef(toRaw(source))
}
}

function propertyToShallowRef(
source: Record<string, any>,
key: string,
defaultValue?: unknown
) {
const val = source[key]
return isRef(val) && isShallow(val)
? val
: (new ObjectRefImpl(
shallowReactive(toRaw(source)),
key,
defaultValue,
true
) as any)
}

// corner case when use narrows type
// Ex. type RelativePath = string & { __brand: unknown }
// RelativePath extends object -> true
Expand Down