This content originally appeared on DEV Community and was authored by Geoffrey Kim
In modern Vue.js applications, performance and efficient state management are crucial. Vue 3 provides a powerful API called shallowRef
that allows developers to fine-tune the reactivity system, especially when working with large data structures or external libraries. In this post, we’ll explore what shallowRef
is, the concept of opting out of deep reactivity, how .value
access is tracked, and practical use cases with code examples.
What is shallowRef
?
Vue’s reactivity system is designed to deeply track changes in objects. When using ref()
, every nested property within an object becomes reactive, meaning that any change—even deep within the object—will trigger reactive updates. However, in some scenarios, deep reactivity can lead to unnecessary overhead or unintended side effects.
Enter shallowRef()
: it creates a reactive reference where only the top-level property is reactive. This means that while changes to the reference itself (i.e., when you assign a new value to .value
) are tracked, modifications to nested properties are not.
Consider the following example:
<script setup>
import { ref, shallowRef } from 'vue';
// Deep reactive ref
const deepRef = ref({ count: 0 });
deepRef.value.count++; // Triggers reactive updates
// Shallow reactive ref
const shallow = shallowRef({ count: 0 });
shallow.value.count++; // Does NOT trigger reactive updates
</script>
In this code:
-
ref()
ensures that changes in any nested property (likecount
) are tracked. -
shallowRef()
only tracks changes when the top-level object is replaced.
What Does "Opt-Out" Mean?
In the context of Vue's reactivity, "opting out" refers to the deliberate choice to disable or avoid the default behavior—in this case, deep reactivity. With shallowRef()
, you are explicitly opting out of the automatic tracking of nested properties. This is especially useful when you want to:
- Reduce performance overhead: By avoiding deep tracking in large or complex objects.
- Integrate with external libraries: Where the internal state management should remain unaffected by Vue's reactivity system.
In essence, shallowRef()
gives you control over which parts of your state should be reactive, ensuring that Vue's reactivity system only tracks what is necessary.
Tracking .value
Access in shallowRef
When using shallowRef()
, only the access of the .value
property is tracked for reactivity. This means:
- If you modify a nested property (e.g.,
shallowRef.value.count++
), Vue will not detect the change. - Only when you replace the entire object assigned to
.value
does Vue trigger reactivity.
Consider this example:
<script setup>
import { shallowRef, watchEffect } from 'vue';
const obj = shallowRef({ count: 0 });
watchEffect(() => {
console.log("Shallow Data Updated:", obj.value.count);
});
obj.value.count++; // This change is not detected
obj.value = { count: 1 }; // This change is detected and triggers watchEffect
</script>
Here, updating obj.value.count
does nothing for the reactivity system. Only when we replace obj.value
entirely does the change propagate.
Practical Use Cases for shallowRef
1. Handling Large Data Structures
When working with large datasets, deep reactivity can be a performance bottleneck. By using shallowRef()
, you ensure that Vue only tracks changes to the top-level object, reducing unnecessary reactivity on every nested property change.
<script setup>
import { ref, shallowRef, watchEffect } from 'vue';
// Deep reactive data (may cause performance issues)
const deepData = ref({ items: new Array(10000).fill({ value: 0 }) });
// Shallow reactive data for performance optimization
const shallowData = shallowRef({ items: new Array(10000).fill({ value: 0 }) });
// Deep reactive watch: triggers on nested changes
watchEffect(() => {
console.log("Deep Data Updated:", deepData.value.items[0].value);
});
deepData.value.items[0].value = 100; // Triggers watchEffect
// Shallow reactive watch: triggers only on top-level changes
watchEffect(() => {
console.log("Shallow Data Updated:", shallowData.value.items);
});
shallowData.value.items[0].value = 100; // Does NOT trigger watchEffect
shallowData.value = { items: new Array(10000).fill({ value: 1 }) }; // Triggers watchEffect
</script>
2. Managing External Library Instances
When integrating external libraries (e.g., ECharts, D3.js), you might want to prevent Vue's reactivity system from interfering with the library's internal state. Using shallowRef()
helps maintain the library's expected behavior.
<script setup>
import { shallowRef, onMounted } from 'vue';
import * as echarts from 'echarts';
const chart = shallowRef(null);
onMounted(() => {
// Initialize the ECharts instance
chart.value = echarts.init(document.getElementById('chart-container'));
// Set chart options
chart.value.setOption({
title: { text: 'ECharts Example' },
xAxis: { type: 'category', data: ['A', 'B', 'C'] },
yAxis: { type: 'value' },
series: [{ type: 'bar', data: [10, 20, 30] }],
});
});
function updateChart() {
chart.value.setOption({
series: [{ type: 'bar', data: [15, 25, 35] }],
});
}
</script>
<template>
<div>
<button @click="updateChart">Update Chart</button>
<div id="chart-container" style="width: 400px; height: 300px;"></div>
</div>
</template>
By using shallowRef()
, we ensure that Vue does not interfere with the ECharts instance’s internal workings.
3. Managing Child Component State
In scenarios where a parent component needs to handle state provided by a child component, deep reactivity might cause unnecessary updates. With shallowRef()
, the parent component will only track changes when the entire object is replaced.
Parent Component (Parent.vue):
<script setup>
import { shallowRef } from 'vue';
import ChildComponent from './ChildComponent.vue';
const childState = shallowRef(null);
function handleChildReady(state) {
childState.value = state;
}
function updateChildState() {
// Replace the entire object to trigger reactivity
childState.value = { count: 99 };
}
</script>
<template>
<div>
<h1>Parent Component</h1>
<button @click="updateChildState">Update Child State</button>
<p>Child State in Parent: {{ childState?.count }}</p>
<ChildComponent @ready="handleChildReady" />
</div>
</template>
Child Component (ChildComponent.vue):
<script setup>
import { ref, onMounted, defineEmits } from 'vue';
const emit = defineEmits(['ready']);
const state = ref({ count: 0 });
onMounted(() => {
emit('ready', state.value);
});
</script>
<template>
<div>
<h2>Child Component</h2>
<p>Count: {{ state.count }}</p>
<button @click="state.count++">Increment</button>
</div>
</template>
In this case, the parent only reacts when the child’s state object is entirely replaced, not when internal properties (like count
) are updated.
Conclusion
The shallowRef
API in Vue.js offers developers a strategic approach to managing reactivity, particularly when deep tracking may lead to performance issues or interfere with external libraries. By opting out of deep reactivity, you can ensure that Vue only tracks top-level changes—allowing for more efficient state management and smoother integration with non-Vue systems.
When working with large datasets, external libraries, or scenarios where child component state is involved, consider using shallowRef()
to optimize your application’s performance and behavior. This fine-grained control over reactivity is one of Vue 3’s many features that empower developers to build highly performant applications.
This content originally appeared on DEV Community and was authored by Geoffrey Kim

Geoffrey Kim | Sciencx (2025-02-15T04:58:44+00:00) Optimizing Vue.js Performance with `shallowRef`: An In-Depth Guide. Retrieved from https://www.scien.cx/2025/02/15/optimizing-vue-js-performance-with-shallowref-an-in-depth-guide-2/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.