Advanced Vue 3 Reactive APIs: shallowRef, triggerRef, customRef, shallowReactive, toRaw, markRaw, and shallowReadonly
This article explains several advanced Vue 3 reactivity APIs—including shallowRef, triggerRef, customRef, shallowReactive, toRaw, markRaw, and shallowReadonly—detailing their differences from standard refs, performance benefits, usage patterns, and providing complete TypeScript code examples for each.
Preface
When reviewing other people's code, I found that most Vue 3 developers only use ref and reactive for handling reactive data in any scenario, but Vue provides additional, more suitable reactive APIs for specific use cases that can improve performance. This article summarizes several advanced Vue 3 reactive APIs with examples.
shallowRef()
Brief: shallowRef creates a shallow reactive reference. Unlike a normal ref , it does not make the inner properties of an object reactive, so changes to nested properties do not trigger re‑rendering. Only changes to the top‑level reference are tracked.
Code example:
<script lang="ts" setup>
import { shallowRef } from 'vue';
const data = shallowRef({ name: '天天鸭', age: 18 });
// Changing the top‑level reference triggers an update
data.value = { name: 'Bob', age: 20 };
// Changing an inner property does NOT trigger an update
data.value.age = 30;
</script>Use this as a performance optimization when the data structure is complex but only the top‑level reference needs to be reactive.
triggerRef()
Brief: triggerRef forces the re‑execution of effects that depend on a shallowRef . It allows you to manually trigger an update after modifying inner properties, increasing flexibility while still keeping the shallow behavior.
Code example:
<script lang="ts" setup>
import { shallowRef, triggerRef } from 'vue';
const data = shallowRef({ name: '天天鸭', age: 18 });
// Inner property change does not trigger update
data.value.age = 30;
// Manually force an update
triggerRef(data);
</script>Remember to call triggerRef after you have mutated the inner object.
customRef()
Brief: customRef lets you create a user‑defined ref with custom get and set logic, enabling advanced patterns such as debouncing, throttling, or asynchronous updates.
Code example (debounced ref):
<script lang="ts" setup>
import { customRef } from 'vue';
function debouncedRef(initialValue, delay) {
let timeoutId;
return customRef((track, trigger) => ({
get() {
track();
return initialValue;
},
set(newValue) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
initialValue = newValue;
trigger();
}, delay);
}
}));
}
const myDebouncedRef = debouncedRef('Hello World', 500);
</script>When set is called, the timer is reset and the value is updated after the delay, triggering dependent effects.
Using the custom ref in a component:
<script lang="ts" setup>
import { onMounted } from 'vue';
import { debouncedRef } from './debouncedRef';
export default {
setup() {
const myDebouncedRef = debouncedRef('Hello World', 500);
onMounted(() => {
console.log(myDebouncedRef.value); // Logs after 500 ms
});
}
};
</script>Note: the object returned by customRef must expose a value property, as required by Vue's ref contract.
shallowReactive()
Brief: shallowReactive works like reactive but only makes the top‑level properties reactive. Nested objects remain non‑reactive, so only changes to the root object trigger updates.
Code example:
<script lang="ts" setup>
import { shallowReactive, isReactive } from 'vue';
const state = shallowReactive({
foo: 1,
nested: { age: 18 }
});
state.foo++; // reactive
// Nested object is not reactive
console.log(isReactive(state.nested)); // false
state.nested.age++; // no re‑render
</script>Useful for performance when deep reactivity is unnecessary.
toRaw()
Brief: toRaw returns the original non‑reactive object behind a reactive or ref proxy, allowing you to bypass Vue's reactivity system.
Code example:
<script lang="ts" setup>
import { reactive, toRaw } from 'vue';
const state = reactive({ count: 0 });
const rawState = toRaw(state);
rawState.count = 10; // does not trigger re‑render
console.log(state.count); // 0
</script>This is handy for performance‑critical sections or when interacting with external libraries that expect plain objects.
markRaw()
Brief: markRaw marks an object so that Vue will never convert it into a reactive proxy, even when passed to reactive or shallowReactive .
Code example:
<script lang="ts" setup>
import { markRaw, reactive } from 'vue';
const someObject = { name: '天天鸭' };
const markedObject = markRaw(someObject);
// Even after wrapping with reactive, it stays non‑reactive
const state = reactive({ obj: markedObject });
</script>Note: markRaw does not apply to ref because ref handles reactivity differently.
shallowReadonly()
Brief: shallowReadonly makes the top‑level properties of an object read‑only while leaving nested objects mutable, similar to readonly but shallow.
Code example:
<script lang="ts" setup>
import { shallowReadonly } from 'vue';
const state = {
name: '天天鸭',
profile: {
age: 18,
address: { city: '广州' }
}
};
const shallowState = shallowReadonly(state);
// Throws error – top‑level is read‑only
shallowState.name = 'change天天鸭';
// Nested objects can still be modified
shallowState.profile.age = 31;
shallowState.profile.address.city = '深圳';
</script>Use this when the first‑level data should remain immutable but deeper structures need to stay mutable.
Conclusion
Although most projects can be built with the basic ref and reactive APIs, applying these advanced Vue 3 reactivity utilities in appropriate scenarios can improve performance and code clarity. Experienced Vue developers should be familiar with them.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.